From 538292e95bf26f3ca41b8e5f8208025f44f418b5 Mon Sep 17 00:00:00 2001 From: Kevin Button Date: Thu, 3 Jul 2025 22:16:48 +0000 Subject: [PATCH 1/2] remove pats dependency --- src/ghstack/ghstack/__init__.py | 10 + src/ghstack/ghstack/__main__.py | 6 + src/ghstack/ghstack/action.py | 44 + src/ghstack/ghstack/cache.py | 76 + src/ghstack/ghstack/checkout.py | 31 + src/ghstack/ghstack/circleci.py | 37 + src/ghstack/ghstack/circleci_real.py | 87 + src/ghstack/ghstack/cli.py | 297 + src/ghstack/ghstack/config.py | 258 + src/ghstack/ghstack/diff.py | 117 + src/ghstack/ghstack/forensics.py | 129 + src/ghstack/ghstack/git.py | 99 + src/ghstack/ghstack/github.py | 97 + src/ghstack/ghstack/github_fake.py | 499 ++ src/ghstack/ghstack/github_real.py | 243 + src/ghstack/ghstack/github_schema.graphql | 9067 +++++++++++++++++++++ src/ghstack/ghstack/github_utils.py | 141 + src/ghstack/ghstack/gpg_sign.py | 41 + src/ghstack/ghstack/land.py | 204 + src/ghstack/ghstack/logs.py | 182 + src/ghstack/ghstack/py.typed | 0 src/ghstack/ghstack/rage.py | 120 + src/ghstack/ghstack/shell.py | 339 + src/ghstack/ghstack/status.py | 146 + src/ghstack/ghstack/submit.py | 1849 +++++ src/ghstack/ghstack/test_prelude.py | 400 + src/ghstack/ghstack/trailers.py | 191 + src/ghstack/ghstack/types.py | 27 + src/ghstack/ghstack/unlink.py | 133 + 29 files changed, 14870 insertions(+) create mode 100644 src/ghstack/ghstack/__init__.py create mode 100755 src/ghstack/ghstack/__main__.py create mode 100644 src/ghstack/ghstack/action.py create mode 100644 src/ghstack/ghstack/cache.py create mode 100644 src/ghstack/ghstack/checkout.py create mode 100644 src/ghstack/ghstack/circleci.py create mode 100644 src/ghstack/ghstack/circleci_real.py create mode 100644 src/ghstack/ghstack/cli.py create mode 100644 src/ghstack/ghstack/config.py create mode 100644 src/ghstack/ghstack/diff.py create mode 100644 src/ghstack/ghstack/forensics.py create mode 100644 src/ghstack/ghstack/git.py create mode 100644 src/ghstack/ghstack/github.py create mode 100644 src/ghstack/ghstack/github_fake.py create mode 100644 src/ghstack/ghstack/github_real.py create mode 100644 src/ghstack/ghstack/github_schema.graphql create mode 100644 src/ghstack/ghstack/github_utils.py create mode 100644 src/ghstack/ghstack/gpg_sign.py create mode 100644 src/ghstack/ghstack/land.py create mode 100644 src/ghstack/ghstack/logs.py create mode 100644 src/ghstack/ghstack/py.typed create mode 100644 src/ghstack/ghstack/rage.py create mode 100644 src/ghstack/ghstack/shell.py create mode 100644 src/ghstack/ghstack/status.py create mode 100644 src/ghstack/ghstack/submit.py create mode 100644 src/ghstack/ghstack/test_prelude.py create mode 100644 src/ghstack/ghstack/trailers.py create mode 100644 src/ghstack/ghstack/types.py create mode 100644 src/ghstack/ghstack/unlink.py diff --git a/src/ghstack/ghstack/__init__.py b/src/ghstack/ghstack/__init__.py new file mode 100644 index 0000000..bb565c9 --- /dev/null +++ b/src/ghstack/ghstack/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +import sys + +if sys.version_info >= (3, 8): + import importlib.metadata as importlib_metadata +else: + import importlib_metadata + +__version__ = importlib_metadata.version("ghstack") # type: ignore[no-untyped-call] diff --git a/src/ghstack/ghstack/__main__.py b/src/ghstack/ghstack/__main__.py new file mode 100755 index 0000000..866b1f4 --- /dev/null +++ b/src/ghstack/ghstack/__main__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from ghstack.cli import main + +if __name__ == "__main__": + main() diff --git a/src/ghstack/ghstack/action.py b/src/ghstack/ghstack/action.py new file mode 100644 index 0000000..ac6e1b6 --- /dev/null +++ b/src/ghstack/ghstack/action.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import logging +from typing import Optional + +import ghstack.github +import ghstack.github_utils +import ghstack.shell + + +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( + """ + query ($owner: String!, $name: String!, $number: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $number) { + id + } + } + } + """, + **params, + ) + pr_id = pr_result["data"]["repository"]["pullRequest"]["id"] + + if close: + logging.info("Closing {owner}/{name}#{number}".format(**params)) + github.graphql( + """ + mutation ($input: ClosePullRequestInput!) { + closePullRequest(input: $input) { + clientMutationId + } + } + """, + input={"pullRequestId": pr_id, "clientMutationId": "A"}, + ) diff --git a/src/ghstack/ghstack/cache.py b/src/ghstack/ghstack/cache.py new file mode 100644 index 0000000..c0eb71b --- /dev/null +++ b/src/ghstack/ghstack/cache.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# A simple, key-value cache. +# - Concurrency safe +# - Handles eviction + +import os +import sqlite3 +from typing import Optional + +_handle: Optional[sqlite3.Connection] = None +CACHE_SIZE = 1000 +CURRENT_VERSION = 1 + + +def _db_conn() -> sqlite3.Connection: + global _handle + fn = os.path.expanduser("~/.ghstackcache") + if not _handle: + _handle = sqlite3.connect(fn) + user_version = _handle.execute("PRAGMA user_version").fetchone() + if user_version is None or user_version[0] != CURRENT_VERSION: + _handle.close() + os.remove(fn) + _handle = sqlite3.connect(fn) + _handle.execute( + """ + CREATE TABLE ghstack_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT, + key TEXT, + value TEXT + ) + """ + ) + _handle.execute( + """ + CREATE UNIQUE INDEX domain_key ON ghstack_cache (domain, key) + """ + ) + _handle.execute("PRAGMA user_version = {}".format(CURRENT_VERSION)) + _handle.commit() + return _handle + + +def get(domain: str, key: str) -> Optional[str]: + conn = _db_conn() + c = conn.execute( + "SELECT value FROM ghstack_cache WHERE domain = ? AND key = ?", (domain, key) + ) + r = c.fetchone() + if r is None: + return None + r = r[0] + assert isinstance(r, str) + return r + + +def put(domain: str, key: str, value: str) -> None: + conn = _db_conn() + conn.execute( + "UPDATE ghstack_cache SET value = ? WHERE domain = ? AND key = ?", + (value, domain, key), + ) + c = conn.execute( + """ + INSERT INTO ghstack_cache (domain, key, value) + SELECT ?, ?, ? WHERE (SELECT Changes() = 0) + """, + (domain, key, value), + ) + if c.lastrowid is not None: + conn.execute( + "DELETE FROM ghstack_cache WHERE id < ?", (c.lastrowid - CACHE_SIZE,) + ) + conn.commit() diff --git a/src/ghstack/ghstack/checkout.py b/src/ghstack/ghstack/checkout.py new file mode 100644 index 0000000..4767297 --- /dev/null +++ b/src/ghstack/ghstack/checkout.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import logging +import re + +import ghstack.github +import ghstack.github_utils +import ghstack.shell + + +def main( + pull_request: str, + github: ghstack.github.GitHubEndpoint, + sh: ghstack.shell.Shell, + remote_name: str, +) -> None: + + params = ghstack.github_utils.parse_pull_request( + pull_request, sh=sh, remote_name=remote_name + ) + head_ref = github.get_head_ref(**params) + orig_ref = re.sub(r"/head$", "/orig", head_ref) + if orig_ref == head_ref: + logging.warning( + "The ref {} doesn't look like a ghstack reference".format(head_ref) + ) + + # TODO: Handle remotes correctly too (so this subsumes hub) + + sh.git("fetch", "--prune", remote_name) + sh.git("checkout", remote_name + "/" + orig_ref) diff --git a/src/ghstack/ghstack/circleci.py b/src/ghstack/ghstack/circleci.py new file mode 100644 index 0000000..18cad79 --- /dev/null +++ b/src/ghstack/ghstack/circleci.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +from abc import ABCMeta, abstractmethod +from typing import Any + + +class CircleCIEndpoint(metaclass=ABCMeta): + async def get(self, path: str, **kwargs: Any) -> Any: + """ + Send a GET request to endpoint 'path'. + + Returns: parsed JSON response + """ + return await self.rest("get", path, **kwargs) + + async def post(self, path: str, **kwargs: Any) -> Any: + """ + Send a POST request to endpoint 'path'. + + Returns: parsed JSON response + """ + return await self.rest("post", path, **kwargs) + + @abstractmethod + async 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, + does NOT include the API version number + **kwargs: dictionary of JSON payload to send + + Returns: parsed JSON response + """ + pass diff --git a/src/ghstack/ghstack/circleci_real.py b/src/ghstack/ghstack/circleci_real.py new file mode 100644 index 0000000..16cf09e --- /dev/null +++ b/src/ghstack/ghstack/circleci_real.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +import json +import logging +import re +from typing import Any, Optional + +import aiohttp + +import ghstack.cache +import ghstack.circleci + +RE_BUILD_PATH = re.compile(r"^project/github/[^/]+/[^/]+/[0-9]+$") + + +class RealCircleCIEndpoint(ghstack.circleci.CircleCIEndpoint): + rest_endpoint: str = "https://circleci.com/api/v1.1" + + # The API token to authenticate to CircleCI with + # https://circleci.com/account/api + circle_token: Optional[str] + + # The URL of a proxy to use for these connections (for + # Facebook users, this is typically 'http://fwdproxy:8080') + proxy: Optional[str] + + def __init__( + self, *, circle_token: Optional[str] = None, proxy: Optional[str] = None + ): + self.circle_token = circle_token + self.proxy = proxy + + async def rest(self, method: str, path: str, **kwargs: Any) -> Any: + headers = { + "Content-Type": "application/json", + "User-Agent": "ghstack", + } + + url = self.rest_endpoint + "/" + path + logging.debug("# {} {}".format(method, url)) + logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1))) + + params = {} + if self.circle_token: + params["circle-token"] = self.circle_token + + is_get_build = method == "get" and RE_BUILD_PATH.match(path) + + if is_get_build: + # consult cache + cache_result = ghstack.cache.get("circleci", path) + if cache_result is not None: + logging.debug("Retrieved result from cache") + return json.loads(cache_result) + + async with aiohttp.request( + method.upper(), + url, + params=params, + json=kwargs, + headers=headers, + proxy=self.proxy, + ) as resp: + logging.debug("Response status: {}".format(resp.status)) + + r_text = await resp.text() + + try: + r = json.loads(r_text) + except json.decoder.JSONDecodeError: + logging.debug("Response body:\n{}".format(r_text)) + raise + else: + pretty_json = json.dumps(r, indent=1) + logging.debug("Response JSON:\n{}".format(pretty_json)) + + try: + resp.raise_for_status() + except aiohttp.ClientResponseError: + raise RuntimeError(pretty_json) + + # NB: Don't save to cache if it's still running + if is_get_build and r["outcome"] is not None: + logging.debug("Saving result to cache") + ghstack.cache.put("circleci", path, r_text) + + return r diff --git a/src/ghstack/ghstack/cli.py b/src/ghstack/ghstack/cli.py new file mode 100644 index 0000000..b178aa1 --- /dev/null +++ b/src/ghstack/ghstack/cli.py @@ -0,0 +1,297 @@ +import asyncio +import contextlib +from typing import Generator, List, Optional, Tuple + +import click + +import ghstack +import ghstack.action +import ghstack.checkout +import ghstack.circleci_real +import ghstack.config +import ghstack.github_real +import ghstack.land +import ghstack.logs +import ghstack.rage +import ghstack.status +import ghstack.submit +import ghstack.unlink + +EXIT_STACK = contextlib.ExitStack() + +GhstackContext = Tuple[ + ghstack.shell.Shell, + ghstack.config.Config, + ghstack.github_real.RealGitHubEndpoint, +] + + +@contextlib.contextmanager +def cli_context( + *, + request_circle_token: bool = False, + request_github_token: bool = True, +) -> Generator[GhstackContext, None, None]: + with EXIT_STACK: + shell = ghstack.shell.Shell() + config = ghstack.config.read_config( + request_circle_token=request_circle_token, + request_github_token=request_github_token, + ) + github = ghstack.github_real.RealGitHubEndpoint( + oauth_token=config.github_oauth, + proxy=config.proxy, + github_url=config.github_url, + ) + yield shell, config, github + + +@click.group(invoke_without_command=True) +@click.pass_context +@click.version_option(ghstack.__version__, "--version", "-V") +@click.option("--debug", is_flag=True, help="Log debug information to stderr") +# hidden arguments that we'll pass along to submit if no other command given +@click.option("--message", "-m", default="Update", hidden=True) +@click.option("--update-fields", "-u", is_flag=True, hidden=True) +@click.option("--short", is_flag=True, hidden=True) +@click.option("--force", is_flag=True, hidden=True) +@click.option("--no-skip", is_flag=True, hidden=True) +@click.option("--draft", is_flag=True, hidden=True) +@click.option( + "--direct/--no-direct", "direct_opt", is_flag=True, hidden=True, default=None +) +@click.option("--base", "-B", default=None, hidden=True) +@click.option("--stack/--no-stack", "-s/-S", is_flag=True, default=True, hidden=True) +def main( + ctx: click.Context, + debug: bool, + message: str, + update_fields: bool, + short: bool, + force: bool, + direct_opt: Optional[bool], + no_skip: bool, + draft: bool, + base: Optional[str], + stack: bool, +) -> None: + """ + Submit stacks of diffs to Github + """ + EXIT_STACK.enter_context(ghstack.logs.manager(debug=debug)) + + if not ctx.invoked_subcommand: + ctx.invoke( + submit, + message=message, + update_fields=update_fields, + short=short, + force=force, + no_skip=no_skip, + draft=draft, + base=base, + stack=stack, + direct_opt=direct_opt, + ) + + +@main.command("action") +@click.option("--close", is_flag=True, help="Close the specified pull request") +@click.argument("pull_request", metavar="PR") +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, + ) + + +@main.command("checkout") +@click.argument("pull_request", metavar="PR") +def checkout(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, + ) + + +@main.command("land") +@click.option("--force", is_flag=True, help="force land even if the PR is closed") +@click.argument("pull_request", metavar="PR") +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, + repo_default_branch_opt=config.repo_default_branch, + ) + + +@main.command("rage") +@click.option( + "--latest", + is_flag=True, + help="Select the last command (not including rage commands) to report", +) +def rage(latest: bool) -> None: + with cli_context(request_github_token=False): + ghstack.rage.main(latest) + + +@main.command("status") +@click.argument("pull_request", metavar="PR") +def status(pull_request: str) -> None: + """ + Check status of a PR + """ + with cli_context(request_circle_token=True) as (shell, config, github): + circleci = ghstack.circleci_real.RealCircleCIEndpoint( + circle_token=config.circle_token + ) + + fut = 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") +@click.option( + "--message", + "-m", + default="Update", + help="Description of change you made", +) +@click.option( + "--update-fields", + "-u", + is_flag=True, + help="Update GitHub pull request summary from the local commit", +) +@click.option( + "--short", is_flag=True, help="Print only the URL of the latest opened PR to stdout" +) +@click.option( + "--force", + is_flag=True, + help="force push the branch even if your local branch is stale", +) +@click.option( + "--no-skip", + is_flag=True, + help="Never skip pushing commits, even if the contents didn't change " + "(use this if you've only updated the commit message).", +) +@click.option( + "--draft", + is_flag=True, + help="Create the pull request in draft mode (only if it has not already been created)", +) +@click.option( + "--base", + "-B", + default=None, + help="Branch to base the stack off of; " + "defaults to the default branch of a repository", +) +@click.option( + "--stack/--no-stack", + "-s/-S", + is_flag=True, + default=True, + help="Submit the entire of stack of commits reachable from HEAD, versus only single commits. " + "This affects the meaning of REVS. With --stack, we submit all commits that " + "are reachable from REVS, excluding commits already on the base branch. Revision ranges " + "supported by git rev-list are also supported. " + "With --no-stack, we support only non-range identifiers, and will submit each commit " + "listed in the command line.", +) +@click.option( + "--direct/--no-direct", + "direct_opt", + default=None, + is_flag=True, + help="Create stack that directly merges into master", +) +@click.argument( + "revs", + nargs=-1, + metavar="REVS", +) +def submit( + message: str, + update_fields: bool, + short: bool, + force: bool, + no_skip: bool, + draft: bool, + direct_opt: Optional[bool], + base: Optional[str], + revs: Tuple[str, ...], + stack: bool, +) -> None: + """ + 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, + repo_name_opt=config.repo_name, + repo_owner_opt=config.repo_owner, + repo_is_fork_opt=config.repo_is_fork, + repo_id_opt=config.repo_id, + repo_default_branch_opt=config.repo_default_branch, + ) + + +@main.command("unlink") +@click.argument("commits", nargs=-1, metavar="COMMIT") +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, + repo_default_branch_opt=config.repo_default_branch, + ) diff --git a/src/ghstack/ghstack/config.py b/src/ghstack/ghstack/config.py new file mode 100644 index 0000000..e8ad17d --- /dev/null +++ b/src/ghstack/ghstack/config.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 + +import configparser +import getpass +import logging +import os +import re +from pathlib import Path +from typing import NamedTuple, Optional + +import requests + +import ghstack.logs + +DEFAULT_GHSTACKRC_PATH = Path.home() / ".ghstackrc" +GHSTACKRC_PATH_VAR = "GHSTACKRC_PATH" + +Config = NamedTuple( + "Config", + [ + # Proxy to use when making connections to GitHub + ("proxy", Optional[str]), + # OAuth token to authenticate to GitHub with + ("github_oauth", Optional[str]), + # GitHub username; used to namespace branches we create + ("github_username", str), + # Token to authenticate to CircleCI with + ("circle_token", Optional[str]), + # These config parameters are not used by ghstack, but other + # tools that reuse this module + # Path to working fbsource checkout + ("fbsource_path", str), + # Path to working git checkout (ghstack infers your git checkout + # based on CWD) + ("github_path", str), + # Path to project directory inside fbsource, to default when + # autodetection fails + ("default_project_dir", str), + # GitHub url. Defaults to github.com which is true for all non-enterprise github repos + ("github_url", str), + # Name of the upstream remote + ("remote_name", str), + # Repository name + ("repo_name", Optional[str]), + # Repository owner / organisation + ("repo_owner", Optional[str]), + # Whether the repo is a fork + ("repo_is_fork", Optional[str]), + # Numeric GitHub repository ID + ("repo_id", Optional[str]), + # Default branch name (e.g. "main") + ("repo_default_branch", Optional[str]), + ], +) + + +def get_path_from_env_var(var_name: str) -> Optional[Path]: + if (path := os.environ.get(var_name)) is not None: + return Path(path).expanduser().resolve() + return None + + +def read_config( + *, + request_circle_token: bool = False, + request_github_token: bool = True, +) -> Config: # noqa: C901 + config = configparser.ConfigParser() + + config_path = None + current_dir = Path(os.getcwd()) + + while current_dir != current_dir.parent: + tentative_config_path = "/".join([str(current_dir), ".ghstackrc"]) + if os.path.exists(tentative_config_path): + config_path = tentative_config_path + break + current_dir = current_dir.parent + + write_back = False + if config_path is None: + config_path = str( + get_path_from_env_var(GHSTACKRC_PATH_VAR) or DEFAULT_GHSTACKRC_PATH + ) + write_back = True + + logging.debug(f"config_path = {config_path}") + config.read([".ghstackrc", config_path]) + + if not config.has_section("ghstack"): + config.add_section("ghstack") + write_back = True + + if config.has_option("ghstack", "github_url"): + github_url = config.get("ghstack", "github_url") + else: + github_url = input("GitHub enterprise domain (leave blank for OSS GitHub): ") + if not github_url: + github_url = "github.com" + if not re.match(r"[\w\.-]+\.\w+$", github_url): + raise RuntimeError( + f"{github_url} is not a valid domain name (do not include http:// scheme)" + ) + config.set("ghstack", "github_url", github_url) + write_back = True + + # Environment variable overrides config file + # This envvar is legacy from ghexport days + github_oauth = os.getenv("OAUTH_TOKEN") + if github_oauth is not None: + logging.warning( + "Deprecated OAUTH_TOKEN environment variable used to populate github_oauth--" + "this is probably not what you intended; unset OAUTH_TOKEN from your " + "environment to use the setting in .ghstackrc instead." + ) + if github_oauth is None and config.has_option("ghstack", "github_oauth"): + github_oauth = config.get("ghstack", "github_oauth") + if github_oauth is None and request_github_token: + print("Generating GitHub access token...") + CLIENT_ID = "89cc88ca50efbe86907a" + res = requests.post( + f"https://{github_url}/login/device/code", + headers={"Accept": "application/json"}, + data={"client_id": CLIENT_ID, "scope": "repo"}, + ) + data = res.json() + print(f"User verification code: {data['user_code']}") + print("Go to https://github.com/login/device and enter the code.") + print("Once you've authorized ghstack, press any key to continue...") + input() + + res = requests.post( + f"https://{github_url}/login/oauth/access_token", + headers={"Accept": "application/json"}, + data={ + "client_id": CLIENT_ID, + "device_code": data["device_code"], + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + }, + ) + github_oauth = res.json()["access_token"] + config.set("ghstack", "github_oauth", github_oauth) + write_back = True + if github_oauth is not None: + ghstack.logs.formatter.redact(github_oauth, "") + + circle_token = None + if circle_token is None and config.has_option("ghstack", "circle_token"): + circle_token = config.get("ghstack", "circle_token") + if circle_token is None and request_circle_token: + circle_token = getpass.getpass( + "CircleCI Personal API token (make one at " + "https://circleci.com/account/api ): " + ).strip() + config.set("ghstack", "circle_token", circle_token) + write_back = True + if circle_token is not None: + ghstack.logs.formatter.redact(circle_token, "") + + github_username = None + if config.has_option("ghstack", "github_username"): + github_username = config.get("ghstack", "github_username") + if github_username is None and github_oauth is not None: + request_url: str + if github_url == "github.com": + request_url = f"https://api.{github_url}/user" + else: + request_url = f"https://{github_url}/api/v3/user" + res = requests.get( + request_url, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {github_oauth}", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + res.raise_for_status() + github_username = res.json()["login"] + config.set("ghstack", "github_username", github_username) + write_back = True + if github_username is None: + github_username = input("GitHub username: ") + if not re.match( + r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", github_username, re.I + ): + raise RuntimeError( + "{} is not a valid GitHub username".format(github_username) + ) + config.set("ghstack", "github_username", github_username) + write_back = True + + proxy = None + if config.has_option("ghstack", "proxy"): + proxy = config.get("ghstack", "proxy") + + if config.has_option("ghstack", "fbsource_path"): + fbsource_path = config.get("ghstack", "fbsource_path") + else: + fbsource_path = os.path.expanduser("~/local/fbsource") + + if config.has_option("ghstack", "github_path"): + github_path = config.get("ghstack", "github_path") + else: + github_path = os.path.expanduser("~/local/ghstack-pytorch") + + if config.has_option("ghstack", "default_project"): + default_project_dir = config.get("ghstack", "default_project_dir") + else: + default_project_dir = "fbcode/caffe2" + + if config.has_option("ghstack", "remote_name"): + remote_name = config.get("ghstack", "remote_name") + else: + remote_name = "origin" + + if write_back: + with open(config_path, "w") as f: + config.write(f) + logging.info("NB: configuration saved to {}".format(config_path)) + + repo_name = None + if config.has_option("repo", "name"): + repo_name = config.get("repo", "name") + + repo_owner = None + if config.has_option("repo", "owner"): + repo_owner = config.get("repo", "owner") + + repo_is_fork = None + if config.has_option("repo", "is_fork"): + repo_is_fork = config.getboolean("repo", "is_fork") + + repo_id = None + if config.has_option("repo", "id"): + repo_id = config.get("repo", "id") + + repo_default_branch = None + if config.has_option("repo", "default_branch"): + repo_default_branch = config.get("repo", "default_branch") + + conf = Config( + github_oauth=github_oauth, + circle_token=circle_token, + github_username=github_username, + proxy=proxy, + fbsource_path=fbsource_path, + github_path=github_path, + default_project_dir=default_project_dir, + github_url=github_url, + remote_name=remote_name, + repo_name=repo_name, + repo_owner=repo_owner, + repo_is_fork=repo_is_fork, + repo_id=repo_id, + repo_default_branch=repo_default_branch, + ) + logging.debug(f"conf = {conf}") + return conf diff --git a/src/ghstack/ghstack/diff.py b/src/ghstack/ghstack/diff.py new file mode 100644 index 0000000..1fd5be7 --- /dev/null +++ b/src/ghstack/ghstack/diff.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import re +from dataclasses import dataclass +from typing import Optional, Pattern + +from ghstack.types import GitHubNumber, GitTreeHash + +RE_GH_METADATA = re.compile( + r"gh-metadata: (?P[^/]+) (?P[^/]+) (?P[0-9]+) " + r"gh/(?P[a-zA-Z0-9-]+)/(?P[0-9]+)/head", + re.MULTILINE, +) + + +RAW_PULL_REQUEST_RESOLVED = ( + r"(Pull Request resolved|Pull-Request-resolved|Pull-Request): " + r"https://{github_url}/(?P[^/]+)/(?P[^/]+)/pull/(?P[0-9]+)" +) + + +def re_pull_request_resolved(github_url: str) -> Pattern[str]: + return re.compile(RAW_PULL_REQUEST_RESOLVED.format(github_url=github_url)) + + +def re_pull_request_resolved_w_sp(github_url: str) -> Pattern[str]: + return re.compile(r"\n*" + RAW_PULL_REQUEST_RESOLVED.format(github_url=github_url)) + + +@dataclass +class PullRequestResolved: + owner: str + repo: str + number: GitHubNumber + github_url: str + + def url(self) -> str: + return "https://{}/{}/{}/pull/{}".format( + self.github_url, self.owner, self.repo, self.number + ) + + @staticmethod + def search(s: str, github_url: str) -> Optional["PullRequestResolved"]: + m = re_pull_request_resolved(github_url).search(s) + if m is not None: + return PullRequestResolved( + owner=m.group("owner"), + repo=m.group("repo"), + number=GitHubNumber(int(m.group("number"))), + github_url=github_url, + ) + m = RE_GH_METADATA.search(s) + if m is not None: + return PullRequestResolved( + owner=m.group("owner"), + repo=m.group("repo"), + number=GitHubNumber(int(m.group("number"))), + github_url=github_url, + ) + return None + + +@dataclass +class Diff: + """ + An abstract representation of a diff. Typically represents git commits, + but we may also virtually be importing diffs from other VCSes, hence + the agnosticism. + """ + + # Title of the diff + title: str + + # Detailed description of the diff. Includes the title. + summary: str + + # Unique identifier representing the commit in question (may be a + # Git/Mercurial commit hash; the important thing is that it can be + # used as a unique identifier.) + oid: str + + # Unique identifier representing the commit in question, but it + # is *invariant* to changes in commit message / summary. In Git, + # a valid identifier would be the tree hash of the commit (rather + # than the commit hash itself); in Phabricator it could be the + # version of the diff. + # + # It is OK for this source id to wobble even if the tree stays the + # same. This simply means we will think there are changes even + # if there aren't any, which should be safe (but just generate + # annoying updates). What we would like is for the id to quiesce: + # if you didn't rebase your hg rev, the source id is guaranteed to + # be the same. + source_id: str + + # The contents of 'Pull-Request'. This is None for + # diffs that haven't been submitted by ghstack. For BC reasons, + # this also accepts gh-metadata. + pull_request_resolved: Optional[PullRequestResolved] + + # A git tree hash that represents the contents of this diff, if it + # were applied in Git. + # + # TODO: Constructing these tree hashes if they're not already in Git + # is a somewhat involved process, as you have to actually construct + # the git tree object (it's not guaranteed to exist already). I'm + # offloading this work onto the ghimport/ghexport tools. + tree: GitTreeHash + + # The name and email of the author, used so we can preserve + # authorship information when constructing a rebased commit + author_name: Optional[str] + author_email: Optional[str] + + # If this isn't actually a diff; it's a boundary commit (not part + # of the stack) that we've parsed for administrative purposes + boundary: bool diff --git a/src/ghstack/ghstack/forensics.py b/src/ghstack/ghstack/forensics.py new file mode 100644 index 0000000..479c7cd --- /dev/null +++ b/src/ghstack/ghstack/forensics.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This script looks at all commits downloads their logs and prints them +# for you + +import re +from typing import Any, Dict + +import aiohttp + +import ghstack.circleci +import ghstack.github +import ghstack.github_utils + +RE_CIRCLECI_URL = re.compile(r"^https://circleci.com/gh/pytorch/pytorch/([0-9]+)") + + +def strip_sccache(x: str) -> str: + sccache_marker = "=================== sccache compilation log ===================" + marker_pos = x.rfind(sccache_marker) + newline_before_marker_pos = x.rfind("\n", 0, marker_pos) + return x[:newline_before_marker_pos] + + +async def main( + pull_request: str, + github: ghstack.github.GitHubEndpoint, + circleci: ghstack.circleci.CircleCIEndpoint, +) -> None: + + # Game plan: + # 1. Query GitHub to find out what the current statuses are + # (TODO: if we got rate limited we'll miss stuff) + # 2. For each status in parallel: + # a. Query CircleCI for job status + # b. (Future work) Query output_url to get log information + # (it's gzip'ed) + # + # For now: + # - Print if the job actually ran, or was skipped + # - Easy way to determine: check if "Should run job after + # checkout" is last step + # - I inspected circleci.get('project/github/pytorch/pytorch/1773555') + # to see if there were other options, there did not appear + # 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) + + # TODO: stop hard-coding number of commits + r = github.graphql( + """ + query ($name: String!, $owner: String!, $number: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $number) { + commits(last: 100) { + nodes { + commit { + oid + messageHeadline + status { + contexts { + context + state + targetUrl + } + } + } + } + } + } + } + } + """, + **params, + ) + nodes = r["data"]["repository"]["pullRequest"]["commits"]["nodes"] + + async def process_node(n: Dict[str, Any]) -> str: + commit = n["commit"] + status = commit["status"] + icon = "❔" + text = "" + buildid_text = "" + if status is not None: + contexts = status["contexts"] + else: + contexts = [] + for c in contexts: + # TODO: Stop hard-coding me + if c["context"] != "ci/circleci: pytorch_linux_xenial_py3_clang5_asan_test": + continue + m = RE_CIRCLECI_URL.match(c["targetUrl"]) + if not m: + icon = "🍆" + break + if c["state"] == "SUCCESS": + icon = "✅" + break + buildid = m.group(1) + buildid_text = " ({})".format(buildid) + r = await circleci.get( + "project/github/{name}/{owner}/{buildid}".format( + buildid=buildid, **params + ) + ) + if not r["failed"]: + # It was just cancelled (don't check "cancelled"; that's + # true even if the job failed otherwise; it just means + # workflow got cancelled) + icon = "❔" + break + icon = "❌" + async with aiohttp.request( + "get", r["steps"][-1]["actions"][-1]["output_url"] + ) as resp: + log_json = await resp.json() + buf = [] + for e in log_json: + buf.append(e["message"]) + text = "\n" + strip_sccache("\n".join(buf)) + text = text[-1500:] + return "{} {} {}{}{}".format( + icon, commit["oid"][:8], commit["messageHeadline"], buildid_text, text + ) + + for n in nodes: + print(await process_node(n)) diff --git a/src/ghstack/ghstack/git.py b/src/ghstack/ghstack/git.py new file mode 100644 index 0000000..ba1ebc4 --- /dev/null +++ b/src/ghstack/ghstack/git.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import re +from functools import cached_property +from typing import List, Pattern + +import ghstack.diff +import ghstack.shell +from ghstack.types import GitCommitHash, GitTreeHash + +RE_RAW_COMMIT_ID = re.compile(r"^(?P-?)(?P[a-f0-9]+)$", re.MULTILINE) +RE_RAW_AUTHOR = re.compile( + r"^author (?P(?P[^<]+?) <(?P[^>]+)>)", re.MULTILINE +) +RE_RAW_PARENT = re.compile(r"^parent (?P[a-f0-9]+)$", re.MULTILINE) +RE_RAW_TREE = re.compile(r"^tree (?P.+)$", re.MULTILINE) +RE_RAW_COMMIT_MSG_LINE = re.compile(r"^ (?P.*)$", re.MULTILINE) + + +class CommitHeader(object): + """ + Represents the information extracted from `git rev-list --header` + """ + + # The unparsed output from git rev-list --header + raw_header: str + + def __init__(self, raw_header: str): + self.raw_header = raw_header + + def _search_group(self, regex: Pattern[str], group: str) -> str: + m = regex.search(self.raw_header) + assert m + return m.group(group) + + @cached_property + def tree(self) -> GitTreeHash: + return GitTreeHash(self._search_group(RE_RAW_TREE, "tree")) + + @cached_property + def title(self) -> str: + return self._search_group(RE_RAW_COMMIT_MSG_LINE, "line") + + @cached_property + def commit_id(self) -> GitCommitHash: + return GitCommitHash(self._search_group(RE_RAW_COMMIT_ID, "commit")) + + @cached_property + def boundary(self) -> bool: + return self._search_group(RE_RAW_COMMIT_ID, "boundary") == "-" + + @cached_property + def parents(self) -> List[GitCommitHash]: + return [ + GitCommitHash(m.group("commit")) + for m in RE_RAW_PARENT.finditer(self.raw_header) + ] + + @cached_property + def author(self) -> str: + return self._search_group(RE_RAW_AUTHOR, "author") + + @cached_property + def author_name(self) -> str: + return self._search_group(RE_RAW_AUTHOR, "name") + + @cached_property + def author_email(self) -> str: + return self._search_group(RE_RAW_AUTHOR, "email") + + @cached_property + def commit_msg(self) -> str: + return "\n".join( + m.group("line") for m in RE_RAW_COMMIT_MSG_LINE.finditer(self.raw_header) + ) + + +def split_header(s: str) -> List[CommitHeader]: + return list(map(CommitHeader, s.split("\0")[:-1])) + + +def convert_header(h: CommitHeader, github_url: str) -> ghstack.diff.Diff: + return ghstack.diff.Diff( + title=h.title, + summary=h.commit_msg, + oid=h.commit_id, + source_id=h.tree, + pull_request_resolved=ghstack.diff.PullRequestResolved.search( + h.raw_header, github_url + ), + tree=h.tree, + author_name=h.author_name, + author_email=h.author_email, + boundary=h.boundary, + ) + + +def parse_header(s: str, github_url: str) -> List[ghstack.diff.Diff]: + return [convert_header(h, github_url) for h in split_header(s)] diff --git a/src/ghstack/ghstack/github.py b/src/ghstack/ghstack/github.py new file mode 100644 index 0000000..31343ca --- /dev/null +++ b/src/ghstack/ghstack/github.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +from abc import ABCMeta, abstractmethod +from typing import Any, Sequence + +import ghstack.diff + + +class NotFoundError(RuntimeError): + pass + + +class GitHubEndpoint(metaclass=ABCMeta): + @abstractmethod + def graphql(self, query: str, **kwargs: Any) -> Any: + """ + Args: + query: string GraphQL query to execute + **kwargs: values for variables in the graphql query + + Returns: parsed JSON response + """ + pass + + 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( + """ + query ($owner: String!, $name: String!, $number: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $number) { + headRefName + } + } + } + """, + **params, + ) + r = pr_result["data"]["repository"]["pullRequest"]["headRefName"] + assert isinstance(r, str), type(r) + return r + + # This hook function should be invoked when a 'git push' to GitHub + # occurs. This is used by testing to simulate actions GitHub + # takes upon branch push, more conveniently than setting up + # a branch hook on the repository and receiving events from it. + # TODO: generalize to any repo + @abstractmethod + def push_hook(self, refName: Sequence[str]) -> None: + pass + + # This should be subsumed by push_hook above, but push_hook is + # annoying to implement and this is more direct + 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.rest("get", path, **kwargs) + + def post(self, path: str, **kwargs: Any) -> Any: + """ + Send a POST request to endpoint 'path'. + + Returns: parsed JSON response + """ + return self.rest("post", path, **kwargs) + + def patch(self, path: str, **kwargs: Any) -> Any: + """ + Send a PATCH request to endpoint 'path'. + + Returns: parsed JSON response + """ + return self.rest("patch", path, **kwargs) + + @abstractmethod + 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 + """ + pass diff --git a/src/ghstack/ghstack/github_fake.py b/src/ghstack/ghstack/github_fake.py new file mode 100644 index 0000000..d82e969 --- /dev/null +++ b/src/ghstack/ghstack/github_fake.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 + +import os.path +import re +from dataclasses import dataclass +from typing import Any, cast, Dict, List, NewType, Optional, Sequence + +import graphql +from typing_extensions import TypedDict + +import ghstack.diff +import ghstack.github +import ghstack.shell + +GraphQLId = NewType("GraphQLId", str) +GitHubNumber = NewType("GitHubNumber", int) +GitObjectID = NewType("GitObjectID", str) + +# https://stackoverflow.com/a/55250601 +SetDefaultBranchInput = TypedDict( + "SetDefaultBranchInput", + { + "name": str, + "default_branch": str, + }, +) + +UpdatePullRequestInput = TypedDict( + "UpdatePullRequestInput", + { + "base": Optional[str], + "title": Optional[str], + "body": Optional[str], + }, +) + +CreatePullRequestInput = TypedDict( + "CreatePullRequestInput", + { + "base": str, + "head": str, + "title": str, + "body": str, + "maintainer_can_modify": bool, + }, +) + +CreateIssueCommentInput = TypedDict( + "CreateIssueCommentInput", + {"body": str}, +) + +CreateIssueCommentPayload = TypedDict( + "CreateIssueCommentPayload", + { + "id": int, + }, +) + +UpdateIssueCommentInput = TypedDict( + "UpdateIssueCommentInput", + {"body": str}, +) + +CreatePullRequestPayload = TypedDict( + "CreatePullRequestPayload", + { + "number": int, + }, +) + + +# The "database" for our mock instance +class GitHubState: + repositories: Dict[GraphQLId, "Repository"] + pull_requests: Dict[GraphQLId, "PullRequest"] + # This is very inefficient but whatever + issue_comments: Dict[GraphQLId, "IssueComment"] + _next_id: int + # These are indexed by repo id + _next_pull_request_number: Dict[GraphQLId, int] + _next_issue_comment_full_database_id: Dict[GraphQLId, int] + root: "Root" + upstream_sh: Optional[ghstack.shell.Shell] + + def repository(self, owner: str, name: str) -> "Repository": + nameWithOwner = "{}/{}".format(owner, name) + for r in self.repositories.values(): + if r.nameWithOwner == nameWithOwner: + return r + raise RuntimeError("unknown repository {}".format(nameWithOwner)) + + def pull_request(self, repo: "Repository", number: GitHubNumber) -> "PullRequest": + for pr in self.pull_requests.values(): + if repo.id == pr._repository and pr.number == number: + return pr + raise RuntimeError( + "unrecognized pull request #{} in repository {}".format( + number, repo.nameWithOwner + ) + ) + + def issue_comment(self, repo: "Repository", comment_id: int) -> "IssueComment": + for comment in self.issue_comments.values(): + if repo.id == comment._repository and comment.fullDatabaseId == comment_id: + return comment + raise RuntimeError( + f"unrecognized issue comment {comment_id} in repository {repo.nameWithOwner}" + ) + + def next_id(self) -> GraphQLId: + r = GraphQLId(str(self._next_id)) + self._next_id += 1 + return r + + def next_pull_request_number(self, repo_id: GraphQLId) -> GitHubNumber: + r = GitHubNumber(self._next_pull_request_number[repo_id]) + self._next_pull_request_number[repo_id] += 1 + return r + + def next_issue_comment_full_database_id(self, repo_id: GraphQLId) -> int: + r = self._next_issue_comment_full_database_id[repo_id] + self._next_issue_comment_full_database_id[repo_id] += 1 + return r + + def push_hook(self, refs: Sequence[str]) -> None: + # updated_refs = set(refs) + # for pr in self.pull_requests: + # # TODO: this assumes only origin repository + # # if pr.headRefName in updated_refs: + # # pr.headRef = + # pass + pass + + def notify_merged(self, pr_resolved: ghstack.diff.PullRequestResolved) -> None: + repo = self.repository(pr_resolved.owner, pr_resolved.repo) + pr = self.pull_request(repo, GitHubNumber(pr_resolved.number)) + pr.closed = True + # TODO: model merged too + + def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: + self.repositories = {} + self.pull_requests = {} + self.issue_comments = {} + self._next_id = 5000 + self._next_pull_request_number = {} + self._next_issue_comment_full_database_id = {} + self.root = Root() + + # Populate it with the most important repo ;) + repo = Repository( + id=GraphQLId("1000"), + name="pytorch", + nameWithOwner="pytorch/pytorch", + isFork=False, + defaultBranchRef=None, + ) + self.repositories[GraphQLId("1000")] = repo + self._next_pull_request_number[GraphQLId("1000")] = 500 + self._next_issue_comment_full_database_id[GraphQLId("1000")] = 1500 + + self.upstream_sh = upstream_sh + if self.upstream_sh is not None: + # Setup upstream Git repository representing the + # pytorch/pytorch repository in the directory specified + # by upstream_sh. This is useful because some GitHub API + # 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") + 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) + + # 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") + + +@dataclass +class Node: + id: GraphQLId + + +GraphQLResolveInfo = Any # for now + + +def github_state(info: GraphQLResolveInfo) -> GitHubState: + context = info.context + assert isinstance(context, GitHubState) + return context + + +@dataclass +class Repository(Node): + name: str + nameWithOwner: str + isFork: bool + defaultBranchRef: Optional["Ref"] + + def pullRequest( + self, info: GraphQLResolveInfo, number: GitHubNumber + ) -> "PullRequest": + return github_state(info).pull_request(self, number) + + def pullRequests(self, info: GraphQLResolveInfo) -> "PullRequestConnection": + return PullRequestConnection( + nodes=list( + filter( + lambda pr: self == pr.repository(info), + github_state(info).pull_requests.values(), + ) + ) + ) + + # 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": + # TODO: Probably should preserve object identity here when + # you call this with refName/oid that are the same + assert state.upstream_sh + gitObject = GitObject( + id=state.next_id(), + # TODO: this upstream_sh hardcode wrong, but ok for now + # because we only have one repo + oid=GitObjectID(state.upstream_sh.git("rev-parse", refName)), + _repository=self.id, + ) + ref = Ref( + id=state.next_id(), + name=refName, + _repository=self.id, + target=gitObject, + ) + return ref + + +@dataclass +class GitObject(Node): + oid: GitObjectID + _repository: GraphQLId + + def repository(self, info: GraphQLResolveInfo) -> Repository: + return github_state(info).repositories[self._repository] + + +@dataclass +class Ref(Node): + name: str + _repository: GraphQLId + target: GitObject + + def repository(self, info: GraphQLResolveInfo) -> Repository: + return github_state(info).repositories[self._repository] + + +@dataclass +class PullRequest(Node): + baseRef: Optional[Ref] + baseRefName: str + body: str + closed: bool + headRef: Optional[Ref] + headRefName: str + # headRepository: Optional[Repository] + # maintainerCanModify: bool + number: GitHubNumber + _repository: GraphQLId # cycle breaker + # state: PullRequestState + title: str + url: str + + def repository(self, info: GraphQLResolveInfo) -> Repository: + return github_state(info).repositories[self._repository] + + +@dataclass +class IssueComment(Node): + body: str + fullDatabaseId: int + _repository: GraphQLId + + def repository(self, info: GraphQLResolveInfo) -> Repository: + return github_state(info).repositories[self._repository] + + +@dataclass +class PullRequestConnection: + nodes: List[PullRequest] + + +class Root: + def repository(self, info: GraphQLResolveInfo, owner: str, name: str) -> Repository: + return github_state(info).repository(owner, name) + + def node(self, info: GraphQLResolveInfo, id: GraphQLId) -> Node: + if id in github_state(info).repositories: + return github_state(info).repositories[id] + elif id in github_state(info).pull_requests: + return github_state(info).pull_requests[id] + elif id in github_state(info).issue_comments: + return github_state(info).issue_comments[id] + else: + raise RuntimeError("unknown id {}".format(id)) + + +with open( + os.path.join(os.path.dirname(__file__), "github_schema.graphql"), encoding="utf-8" +) as f: + GITHUB_SCHEMA = graphql.build_schema(f.read()) + + +# Ummm. I thought there would be a way to stick these on the objects +# themselves (in the same way resolvers can be put on resolvers) but +# after a quick read of default_resolve_type_fn it doesn't look like +# we ever actually look to value for type of information. This is +# pretty clunky lol. +def set_is_type_of(name: str, cls: Any) -> None: + # Can't use a type ignore on the next line because fbcode + # and us don't agree that it's necessary hmm. + o: Any = GITHUB_SCHEMA.get_type(name) + o.is_type_of = lambda obj, info: isinstance(obj, cls) + + +set_is_type_of("Repository", Repository) +set_is_type_of("PullRequest", PullRequest) +set_is_type_of("IssueComment", IssueComment) + + +class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint): + state: GitHubState + + 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( + schema=GITHUB_SCHEMA, + source=query, + root_value=self.state.root, + context_value=self.state, + 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: + self.state.push_hook(refNames) + + 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, + } + + # 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"] + + def _create_issue_comment( + self, owner: str, name: str, comment_id: int, input: CreateIssueCommentInput + ) -> CreateIssueCommentPayload: + state = self.state + id = state.next_id() + repo = state.repository(owner, name) + comment_id = state.next_issue_comment_full_database_id(repo.id) + comment = IssueComment( + id=id, + _repository=repo.id, + fullDatabaseId=comment_id, + body=input["body"], + ) + state.issue_comments[id] = comment + # This is only a subset of what the actual REST endpoint + # returns. + return { + "id": comment_id, + } + + def _update_issue_comment( + self, owner: str, name: str, comment_id: int, input: UpdateIssueCommentInput + ) -> None: + state = self.state + repo = state.repository(owner, name) + comment = state.issue_comment(repo, comment_id) + 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"]) + + def rest(self, method: str, path: str, **kwargs: Any) -> Any: + if method == "get": + m = re.match(r"^repos/([^/]+)/([^/]+)/branches/([^/]+)/protection", path) + if m: + # For now, pretend all branches are not protected + raise ghstack.github.NotFoundError() + + 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), + m.group(2), + GitHubNumber(int(m.group(3))), + cast(CreateIssueCommentInput, kwargs), + ) + 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), + 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/ghstack/github_real.py b/src/ghstack/ghstack/github_real.py new file mode 100644 index 0000000..600fe64 --- /dev/null +++ b/src/ghstack/ghstack/github_real.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +import json +import logging +import re +import time +from typing import Any, Dict, Optional, Sequence, Tuple, Union + +import requests + +import ghstack.github + +MAX_RETRIES = 5 +INITIAL_BACKOFF_SECONDS = 60 + + +class RealGitHubEndpoint(ghstack.github.GitHubEndpoint): + """ + A class representing a GitHub endpoint we can send queries to. + It supports both GraphQL and REST interfaces. + """ + + # The URL of the GraphQL endpoint to connect to + @property + def graphql_endpoint(self) -> str: + if self.github_url == "github.com": + return f"https://api.{self.github_url}/graphql" + else: + return f"https://{self.github_url}/api/graphql" + + # The base URL of the REST endpoint to connect to (all REST requests + # will be subpaths of this URL) + @property + def rest_endpoint(self) -> str: + if self.github_url == "github.com": + return f"https://api.{self.github_url}" + else: + return f"https://{self.github_url}/api/v3" + + # The base URL of regular WWW website, in case we need to manually + # interact with the real website + www_endpoint: str = "https://{github_url}" + + # The string OAuth token to authenticate to the GraphQL server with. + # May be None if we're doing public access only. + oauth_token: Optional[str] + + # The URL of a proxy to use for these connections (for + # Facebook users, this is typically 'http://fwdproxy:8080') + proxy: Optional[str] + + # The certificate bundle to be used to verify the connection. + # Passed to requests as 'verify'. + verify: Optional[str] + + # Client side certificate to use when connecitng. + # Passed to requests as 'cert'. + cert: Optional[Union[str, Tuple[str, str]]] + + def __init__( + self, + oauth_token: Optional[str], + github_url: str, + proxy: Optional[str] = None, + verify: Optional[str] = None, + cert: Optional[Union[str, Tuple[str, str]]] = None, + ): + self.oauth_token = oauth_token + self.proxy = proxy + self.github_url = github_url + self.verify = verify + self.cert = cert + + def push_hook(self, refName: Sequence[str]) -> None: + pass + + def graphql(self, query: str, **kwargs: Any) -> Any: + headers = {} + if self.oauth_token: + headers["Authorization"] = "bearer {}".format(self.oauth_token) + + logging.debug( + "# POST {}".format(self.graphql_endpoint.format(github_url=self.github_url)) + ) + logging.debug("Request GraphQL query:\n{}".format(query)) + logging.debug( + "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)) + + 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) + + if "errors" in r: + raise RuntimeError(pretty_json) + + 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: + return 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 + if m := re.search(r' Any: + assert self.oauth_token + headers = { + "Authorization": "token " + self.oauth_token, + "Content-Type": "application/json", + "User-Agent": "ghstack", + "Accept": "application/vnd.github.v3+json", + } + + url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path + + backoff_seconds = INITIAL_BACKOFF_SECONDS + for attempt in range(0, MAX_RETRIES): + logging.debug("# {} {}".format(method, url)) + logging.debug("Request body:\n{}".format(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("Response status: {}".format(resp.status_code)) + + try: + r = resp.json() + except ValueError: + logging.debug("Response body:\n{}".format(r.text)) + raise + else: + pretty_json = json.dumps(r, indent=1) + logging.debug("Response JSON:\n{}".format(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") diff --git a/src/ghstack/ghstack/github_schema.graphql b/src/ghstack/ghstack/github_schema.graphql new file mode 100644 index 0000000..436f0d7 --- /dev/null +++ b/src/ghstack/ghstack/github_schema.graphql @@ -0,0 +1,9067 @@ +# Yoinked from https://github.com/octokit/graphql-schema +# at commit c5dac09a12478fbd51b1df2e7261067e79afd81c + +# Copyright (c) 2017 Gregor Martynus +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# Autogenerated input type of AcceptTopicSuggestion +input AcceptTopicSuggestionInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of the suggested topic. + name: String! + + # The Node ID of the repository. + repositoryId: ID! +} + +# Autogenerated return type of AcceptTopicSuggestion +type AcceptTopicSuggestionPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The accepted topic. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `topic` will change from `Topic!` to `Topic`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + topic: Topic! +} + +# Represents an object which can take actions on GitHub. Typically a User or Bot. +interface Actor { + # A URL pointing to the actor's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + + # The username of the actor. + login: String! + + # The HTTP path for this actor. + resourcePath: URI! + + # The HTTP URL for this actor. + url: URI! +} + +# Autogenerated input type of AddComment +input AddCommentInput { + # The contents of the comment. + body: String! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the subject to modify. + subjectId: ID! +} + +# Autogenerated return type of AddComment +type AddCommentPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The edge from the subject's comment connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `commentEdge` will change from `IssueCommentEdge!` to `IssueCommentEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + commentEdge: IssueCommentEdge! + + # The subject + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `subject` will change from `Node!` to `Node`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + subject: Node! + + # The edge from the subject's timeline connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `timelineEdge` will change from `IssueTimelineItemEdge!` to `IssueTimelineItemEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + timelineEdge: IssueTimelineItemEdge! +} + +# Autogenerated input type of AddProjectCard +input AddProjectCardInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The content of the card. Must be a member of the ProjectCardItem union + contentId: ID + + # The note on the card. + note: String + + # The Node ID of the ProjectColumn. + projectColumnId: ID! +} + +# Autogenerated return type of AddProjectCard +type AddProjectCardPayload { + # The edge from the ProjectColumn's card connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + cardEdge: ProjectCardEdge! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The ProjectColumn + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `projectColumn` will change from `Project!` to `Project`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + projectColumn: Project! +} + +# Autogenerated input type of AddProjectColumn +input AddProjectColumnInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of the column. + name: String! + + # The Node ID of the project. + projectId: ID! +} + +# Autogenerated return type of AddProjectColumn +type AddProjectColumnPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The edge from the project's column connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + columnEdge: ProjectColumnEdge! + + # The project + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `project` will change from `Project!` to `Project`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + project: Project! +} + +# Autogenerated input type of AddPullRequestReviewComment +input AddPullRequestReviewCommentInput { + # The text of the comment. + body: String! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The SHA of the commit to comment on. + commitOID: GitObjectID + + # The comment id to reply to. + inReplyTo: ID + + # The relative path of the file to comment on. + path: String + + # The line index in the diff to comment on. + position: Int + + # The Node ID of the review to modify. + pullRequestReviewId: ID! +} + +# Autogenerated return type of AddPullRequestReviewComment +type AddPullRequestReviewCommentPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The newly created comment. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `comment` will change from `PullRequestReviewComment!` to `PullRequestReviewComment`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + comment: PullRequestReviewComment! + + # The edge from the review's comment connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `commentEdge` will change from + # `PullRequestReviewCommentEdge!` to `PullRequestReviewCommentEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + commentEdge: PullRequestReviewCommentEdge! +} + +# Autogenerated input type of AddPullRequestReview +input AddPullRequestReviewInput { + # The contents of the review body comment. + body: String + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The review line comments. + comments: [DraftPullRequestReviewComment] + + # The commit OID the review pertains to. + commitOID: GitObjectID + + # The event to perform on the pull request review. + event: PullRequestReviewEvent + + # The Node ID of the pull request to modify. + pullRequestId: ID! +} + +# Autogenerated return type of AddPullRequestReview +type AddPullRequestReviewPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The newly created pull request review. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReview: PullRequestReview! + + # The edge from the pull request's review connection. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `reviewEdge` will change from `PullRequestReviewEdge!` to `PullRequestReviewEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + reviewEdge: PullRequestReviewEdge! +} + +# Autogenerated input type of AddReaction +input AddReactionInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of the emoji to react with. + content: ReactionContent! + + # The Node ID of the subject to modify. + subjectId: ID! +} + +# Autogenerated return type of AddReaction +type AddReactionPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The reaction object. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + reaction: Reaction! + + # The reactable subject. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + subject: Reactable! +} + +# Autogenerated input type of AddStar +input AddStarInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Starrable ID to star. + starrableId: ID! +} + +# Autogenerated return type of AddStar +type AddStarPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The starrable. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + starrable: Starrable! +} + +# Represents a 'added_to_project' event on a given issue or pull request. +type AddedToProjectEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# A GitHub App. +type App implements Node { + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + + # The description of the app. + description: String + id: ID! + + # The hex color code, without the leading '#', for the logo background. + logoBackgroundColor: String! + + # A URL pointing to the app's logo. + logoUrl( + # The size of the resulting image. + size: Int + ): URI! + + # The name of the app. + name: String! + + # A slug based on the name of the app for use in URLs. + slug: String! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The URL to the app's homepage. + url: URI! +} + +# An edge in a connection. +type AppEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: App +} + +# An object that can have users assigned to it. +interface Assignable { + # A list of Users assigned to this object. + assignees( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! +} + +# Represents an 'assigned' event on any assignable object. +type AssignedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the assignable associated with the event. + assignable: Assignable! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the user who was assigned. + user: User +} + +# Represents a 'base_ref_changed' event on a given issue or pull request. +type BaseRefChangedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# Represents a 'base_ref_force_pushed' event on a given pull request. +type BaseRefForcePushedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the after commit SHA for the 'base_ref_force_pushed' event. + afterCommit: Commit + + # Identifies the before commit SHA for the 'base_ref_force_pushed' event. + beforeCommit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # Identifies the fully qualified ref name for the 'base_ref_force_pushed' event. + ref: Ref +} + +# Represents a Git blame. +type Blame { + # The list of ranges from a Git blame. + ranges: [BlameRange!]! +} + +# Represents a range of information from a Git blame. +type BlameRange { + # Identifies the recency of the change, from 1 (new) to 10 (old). This is + # calculated as a 2-quantile and determines the length of distance between the + # median age of all the changes in the file and the recency of the current + # range's change. + age: Int! + + # Identifies the line author + commit: Commit! + + # The ending line for the range + endingLine: Int! + + # The starting line for the range + startingLine: Int! +} + +# Represents a Git blob. +type Blob implements GitObject & Node { + # An abbreviated version of the Git object ID + abbreviatedOid: String! + + # Byte size of Blob object + byteSize: Int! + + # The HTTP path for this Git object + commitResourcePath: URI! + + # The HTTP URL for this Git object + commitUrl: URI! + id: ID! + + # Indicates whether the Blob is binary or text + isBinary: Boolean! + + # Indicates whether the contents is truncated + isTruncated: Boolean! + + # The Git object ID + oid: GitObjectID! + + # The Repository the Git object belongs to + repository: Repository! + + # UTF8 text data or null if the Blob is binary + text: String +} + +# A special type of user which takes actions on behalf of GitHub Apps. +type Bot implements Actor & Node & UniformResourceLocatable { + # A URL pointing to the GitHub App's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # The username of the actor. + login: String! + + # The HTTP path for this bot + resourcePath: URI! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this bot + url: URI! +} + +# An object that can be closed +interface Closable { + # `true` if the object is closed (definition of closed may depend on type) + closed: Boolean! + + # Identifies the date and time when the object was closed. + closedAt: DateTime +} + +# Represents a 'closed' event on any `Closable`. +type ClosedEvent implements Node & UniformResourceLocatable { + # Identifies the actor who performed the event. + actor: Actor + + # Object that was closed. + closable: Closable! + + # Object which triggered the creation of this event. + closer: Closer + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # The HTTP path for this closed event. + resourcePath: URI! + + # The HTTP URL for this closed event. + url: URI! +} + +# The object which triggered a `ClosedEvent`. +union Closer = Commit | PullRequest + +# The Code of Conduct for a repository +type CodeOfConduct { + # The body of the CoC + body: String + + # The key for the CoC + key: String! + + # The formal name of the CoC + name: String! + + # The path to the CoC + url: URI +} + +# Collaborators affiliation level with a subject. +enum CollaboratorAffiliation { + # All collaborators the authenticated user can see. + ALL + + # All collaborators with permissions to an organization-owned subject, regardless of organization membership status. + DIRECT + + # All outside collaborators of an organization-owned subject. + OUTSIDE +} + +# Types that can be inside Collection Items. +union CollectionItemContent = Organization | Repository | User + +# Represents a comment. +interface Comment { + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # The body as Markdown. + body: String! + + # The body rendered to HTML. + bodyHTML: HTML! + + # The body rendered to text. + bodyText: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies when the comment was published at. + publishedAt: DateTime + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# A comment author association with repository. +enum CommentAuthorAssociation { + # Author has been invited to collaborate on the repository. + COLLABORATOR + + # Author has previously committed to the repository. + CONTRIBUTOR + + # Author has not previously committed to GitHub. + FIRST_TIMER + + # Author has not previously committed to the repository. + FIRST_TIME_CONTRIBUTOR + + # Author is a member of the organization that owns the repository. + MEMBER + + # Author has no association with the repository. + NONE + + # Author is the owner of the repository. + OWNER +} + +# The possible errors that will prevent a user from updating a comment. +enum CommentCannotUpdateReason { + # You must be the author or have write access to this repository to update this comment. + INSUFFICIENT_ACCESS + + # Unable to create comment because issue is locked. + LOCKED + + # You must be logged in to update this comment. + LOGIN_REQUIRED + + # Repository is under maintenance. + MAINTENANCE + + # At least one email address must be verified to update this comment. + VERIFIED_EMAIL_REQUIRED +} + +# Represents a 'comment_deleted' event on a given issue or pull request. +type CommentDeletedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# Represents a Git commit. +type Commit implements GitObject & Node & Subscribable { + # An abbreviated version of the Git object ID + abbreviatedOid: String! + + # The number of additions in this commit. + additions: Int! + + # Authorship details of the commit. + author: GitActor + + # Check if the committer and the author match. + authoredByCommitter: Boolean! + + # The datetime when this commit was authored. + authoredDate: DateTime! + + # Fetches `git blame` information. + blame( + # The file whose Git blame information you want. + path: String! + ): Blame! + + # The number of changed files in this commit. + changedFiles: Int! + + # Comments made on the commit. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): CommitCommentConnection! + + # The HTTP path for this Git object + commitResourcePath: URI! + + # The HTTP URL for this Git object + commitUrl: URI! + + # The datetime when this commit was committed. + committedDate: DateTime! + + # Check if commited via GitHub web UI. + committedViaWeb: Boolean! + + # Committership details of the commit. + committer: GitActor + + # The number of deletions in this commit. + deletions: Int! + + # The linear commit history starting from (and including) this commit, in the same order as `git log`. + history( + # Returns the elements in the list that come after the specified cursor. + after: String + + # If non-null, filters history to only show commits with matching authorship. + author: CommitAuthor + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # If non-null, filters history to only show commits touching files under this path. + path: String + + # Allows specifying a beginning time or date for fetching commits. + since: GitTimestamp + + # Allows specifying an ending time or date for fetching commits. + until: GitTimestamp + ): CommitHistoryConnection! + id: ID! + + # The Git commit message + message: String! + + # The Git commit message body + messageBody: String! + + # The commit message body rendered to HTML. + messageBodyHTML: HTML! + + # The Git commit message headline + messageHeadline: String! + + # The commit message headline rendered to HTML. + messageHeadlineHTML: HTML! + + # The Git object ID + oid: GitObjectID! + + # The parents of a commit. + parents( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): CommitConnection! + + # The datetime when this commit was pushed. + pushedDate: DateTime + + # The Repository this commit belongs to + repository: Repository! + + # The HTTP path for this commit + resourcePath: URI! + + # Commit signing information, if present. + signature: GitSignature + + # Status information for this commit + status: Status + + # Returns a URL to download a tarball archive for a repository. + # Note: For private repositories, these links are temporary and expire after five minutes. + tarballUrl: URI! + + # Commit's root Tree + tree: Tree! + + # The HTTP path for the tree of this commit + treeResourcePath: URI! + + # The HTTP URL for the tree of this commit + treeUrl: URI! + + # The HTTP URL for this commit + url: URI! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState + + # Returns a URL to download a zipball archive for a repository. + # Note: For private repositories, these links are temporary and expire after five minutes. + zipballUrl: URI! +} + +# Specifies an author for filtering Git commits. +input CommitAuthor { + # Email addresses to filter by. Commits authored by any of the specified email addresses will be returned. + emails: [String!] + + # ID of a User to filter by. If non-null, only commits authored by this user + # will be returned. This field takes precedence over emails. + id: ID +} + +# Represents a comment on a given Commit. +type CommitComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # Identifies the comment body. + body: String! + + # Identifies the comment body rendered to HTML. + bodyHTML: HTML! + + # The body rendered to text. + bodyText: String! + + # Identifies the commit associated with the comment, if the commit exists. + commit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies the file path associated with the comment. + path: String + + # Identifies the line position associated with the comment. + position: Int + + # Identifies when the comment was published at. + publishedAt: DateTime + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # The repository associated with this node. + repository: Repository! + + # The HTTP path permalink for this commit comment. + resourcePath: URI! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL permalink for this commit comment. + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! + + # Can user react to this subject + viewerCanReact: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# The connection type for CommitComment. +type CommitCommentConnection { + # A list of edges. + edges: [CommitCommentEdge] + + # A list of nodes. + nodes: [CommitComment] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type CommitCommentEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: CommitComment +} + +# A thread of comments on a commit. +type CommitCommentThread implements Node & RepositoryNode { + # The comments that exist in this thread. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): CommitCommentConnection! + + # The commit the comments were made on. + commit: Commit! + id: ID! + + # The file the comments were made on. + path: String + + # The position in the diff for the commit that the comment was made on. + position: Int + + # The repository associated with this node. + repository: Repository! +} + +# The connection type for Commit. +type CommitConnection { + # A list of edges. + edges: [CommitEdge] + + # A list of nodes. + nodes: [Commit] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type CommitEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Commit +} + +# The connection type for Commit. +type CommitHistoryConnection { + edges: [CommitEdge] + + # A list of nodes. + nodes: [Commit] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a 'converted_note_to_issue' event on a given issue or pull request. +type ConvertedNoteToIssueEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# Autogenerated input type of CreateProject +input CreateProjectInput { + # The description of project. + body: String + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of project. + name: String! + + # The owner ID to create the project under. + ownerId: ID! +} + +# Autogenerated return type of CreateProject +type CreateProjectPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The new project. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `project` will change from `Project!` to `Project`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + project: Project! +} + +# Represents a mention made by one issue or pull request to another. +type CrossReferencedEvent implements Node & UniformResourceLocatable { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Reference originated in a different repository. + isCrossRepository: Boolean! + + # Identifies when the reference was made. + referencedAt: DateTime! + + # The HTTP path for this pull request. + resourcePath: URI! + + # Issue or pull request that made the reference. + source: ReferencedSubject! + + # Issue or pull request to which the reference was made. + target: ReferencedSubject! + + # The HTTP URL for this pull request. + url: URI! + + # Checks if the target will be closed when the source is merged. + willCloseTarget: Boolean! +} + +# An ISO-8601 encoded date string. +scalar Date + +# An ISO-8601 encoded UTC date string. +scalar DateTime + +# Autogenerated input type of DeclineTopicSuggestion +input DeclineTopicSuggestionInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of the suggested topic. + name: String! + + # The reason why the suggested topic is declined. + reason: TopicSuggestionDeclineReason! + + # The Node ID of the repository. + repositoryId: ID! +} + +# Autogenerated return type of DeclineTopicSuggestion +type DeclineTopicSuggestionPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The declined topic. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `topic` will change from `Topic!` to `Topic`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + topic: Topic! +} + +# The possible default permissions for repositories. +enum DefaultRepositoryPermissionField { + # Can read, write, and administrate repos by default + ADMIN + + # No access + NONE + + # Can read repos by default + READ + + # Can read and write repos by default + WRITE +} + +# Entities that can be deleted. +interface Deletable { + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! +} + +# Autogenerated input type of DeleteProjectCard +input DeleteProjectCardInput { + # The id of the card to delete. + cardId: ID! + + # A unique identifier for the client performing the mutation. + clientMutationId: String +} + +# Autogenerated return type of DeleteProjectCard +type DeleteProjectCardPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The column the deleted card was in. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `column` will change from `ProjectColumn!` to `ProjectColumn`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + column: ProjectColumn! + + # The deleted card ID. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `deletedCardId` will change from `ID!` to `ID`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + deletedCardId: ID! +} + +# Autogenerated input type of DeleteProjectColumn +input DeleteProjectColumnInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The id of the column to delete. + columnId: ID! +} + +# Autogenerated return type of DeleteProjectColumn +type DeleteProjectColumnPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The deleted column ID. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `deletedColumnId` will change from `ID!` to `ID`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + deletedColumnId: ID! + + # The project the deleted column was in. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `project` will change from `Project!` to `Project`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + project: Project! +} + +# Autogenerated input type of DeleteProject +input DeleteProjectInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Project ID to update. + projectId: ID! +} + +# Autogenerated return type of DeleteProject +type DeleteProjectPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The repository or organization the project was removed from. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `owner` will change from `ProjectOwner!` to `ProjectOwner`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + owner: ProjectOwner! +} + +# Autogenerated input type of DeletePullRequestReview +input DeletePullRequestReviewInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the pull request review to delete. + pullRequestReviewId: ID! +} + +# Autogenerated return type of DeletePullRequestReview +type DeletePullRequestReviewPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The deleted pull request review. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReview: PullRequestReview! +} + +# Represents a 'demilestoned' event on a given issue or pull request. +type DemilestonedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the milestone title associated with the 'demilestoned' event. + milestoneTitle: String! + + # Object referenced by event. + subject: MilestoneItem! +} + +# A repository deploy key. +type DeployKey implements Node { + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # The deploy key. + key: String! + + # Whether or not the deploy key is read only. + readOnly: Boolean! + + # The deploy key title. + title: String! + + # Whether or not the deploy key has been verified. + verified: Boolean! +} + +# The connection type for DeployKey. +type DeployKeyConnection { + # A list of edges. + edges: [DeployKeyEdge] + + # A list of nodes. + nodes: [DeployKey] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type DeployKeyEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: DeployKey +} + +# Represents a 'deployed' event on a given pull request. +type DeployedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + + # The deployment associated with the 'deployed' event. + deployment: Deployment! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # The ref associated with the 'deployed' event. + ref: Ref +} + +# Represents triggered deployment instance. +type Deployment implements Node { + # Identifies the commit sha of the deployment. + commit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the actor who triggered the deployment. + creator: Actor + + # Identifies the primary key from the database. + databaseId: Int + + # The deployment description. + description: String + + # The environment to which this deployment was made. + environment: String + id: ID! + + # The latest status of this deployment. + latestStatus: DeploymentStatus + + # Extra information that a deployment system might need. + payload: String + + # Identifies the Ref of the deployment, if the deployment was created by ref. + ref: Ref + + # Identifies the repository associated with the deployment. + repository: Repository! + + # The current state of the deployment. + state: DeploymentState + + # A list of statuses associated with the deployment. + statuses( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): DeploymentStatusConnection + + # The deployment task. + task: String + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! +} + +# The connection type for Deployment. +type DeploymentConnection { + # A list of edges. + edges: [DeploymentEdge] + + # A list of nodes. + nodes: [Deployment] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type DeploymentEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Deployment +} + +# Represents a 'deployment_environment_changed' event on a given pull request. +type DeploymentEnvironmentChangedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The deployment status that updated the deployment environment. + deploymentStatus: DeploymentStatus! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! +} + +# The possible states in which a deployment can be. +enum DeploymentState { + # The pending deployment was not updated after 30 minutes. + ABANDONED + + # The deployment is currently active. + ACTIVE + + # An inactive transient deployment. + DESTROYED + + # The deployment experienced an error. + ERROR + + # The deployment has failed. + FAILURE + + # The deployment is inactive. + INACTIVE + + # The deployment is pending. + PENDING +} + +# Describes the status of a given deployment attempt. +type DeploymentStatus implements Node { + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the actor who triggered the deployment. + creator: Actor + + # Identifies the deployment associated with status. + deployment: Deployment! + + # Identifies the description of the deployment. + description: String + + # Identifies the environment URL of the deployment. + environmentUrl: URI + id: ID! + + # Identifies the log URL of the deployment. + logUrl: URI + + # Identifies the current state of the deployment. + state: DeploymentStatusState! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! +} + +# The connection type for DeploymentStatus. +type DeploymentStatusConnection { + # A list of edges. + edges: [DeploymentStatusEdge] + + # A list of nodes. + nodes: [DeploymentStatus] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type DeploymentStatusEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: DeploymentStatus +} + +# The possible states for a deployment status. +enum DeploymentStatusState { + # The deployment experienced an error. + ERROR + + # The deployment has failed. + FAILURE + + # The deployment is inactive. + INACTIVE + + # The deployment is pending. + PENDING + + # The deployment was successful. + SUCCESS +} + +# Autogenerated input type of DismissPullRequestReview +input DismissPullRequestReviewInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The contents of the pull request review dismissal message. + message: String! + + # The Node ID of the pull request review to modify. + pullRequestReviewId: ID! +} + +# Autogenerated return type of DismissPullRequestReview +type DismissPullRequestReviewPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The dismissed pull request review. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReview: PullRequestReview! +} + +# Specifies a review comment to be left with a Pull Request Review. +input DraftPullRequestReviewComment { + # Body of the comment to leave. + body: String! + + # Path to the file being commented on. + path: String! + + # Position in the file to leave a comment on. + position: Int! +} + +# An external identity provisioned by SAML SSO or SCIM. +type ExternalIdentity implements Node { + # The GUID for this identity + guid: String! + id: ID! + + # Organization invitation for this SCIM-provisioned external identity + organizationInvitation: OrganizationInvitation + + # SAML Identity attributes + samlIdentity: ExternalIdentitySamlAttributes + + # SCIM Identity attributes + scimIdentity: ExternalIdentityScimAttributes + + # User linked to this external identity. Will be NULL if this identity has not been claimed by an organization member. + user: User +} + +# The connection type for ExternalIdentity. +type ExternalIdentityConnection { + # A list of edges. + edges: [ExternalIdentityEdge] + + # A list of nodes. + nodes: [ExternalIdentity] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ExternalIdentityEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ExternalIdentity +} + +# SAML attributes for the External Identity +type ExternalIdentitySamlAttributes { + # The NameID of the SAML identity + nameId: String +} + +# SCIM attributes for the External Identity +type ExternalIdentityScimAttributes { + # The userName of the SCIM identity + username: String +} + +# The connection type for User. +type FollowerConnection { + # A list of edges. + edges: [UserEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# The connection type for User. +type FollowingConnection { + # A list of edges. + edges: [UserEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# A Gist. +type Gist implements Node & Starrable { + # A list of comments associated with the gist + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): GistCommentConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The gist description. + description: String + id: ID! + + # Whether the gist is public or not. + isPublic: Boolean! + + # The gist name. + name: String! + + # The gist owner. + owner: RepositoryOwner + + # Identifies when the gist was last pushed to. + pushedAt: DateTime + + # A list of users who have starred this starrable. + stargazers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: StarOrder + ): StargazerConnection! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # Returns a boolean indicating whether the viewing user has starred this starrable. + viewerHasStarred: Boolean! +} + +# Represents a comment on an Gist. +type GistComment implements Comment & Deletable & Node & Updatable & UpdatableComment { + # The actor who authored the comment. + author: Actor + + # Author's association with the gist. + authorAssociation: CommentAuthorAssociation! + + # Identifies the comment body. + body: String! + + # The comment body rendered to HTML. + bodyHTML: HTML! + + # The body rendered to text. + bodyText: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The actor who edited the comment. + editor: Actor + + # The associated gist. + gist: Gist! + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies when the comment was published at. + publishedAt: DateTime + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# The connection type for GistComment. +type GistCommentConnection { + # A list of edges. + edges: [GistCommentEdge] + + # A list of nodes. + nodes: [GistComment] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type GistCommentEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: GistComment +} + +# The connection type for Gist. +type GistConnection { + # A list of edges. + edges: [GistEdge] + + # A list of nodes. + nodes: [Gist] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type GistEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Gist +} + +# Ordering options for gist connections +input GistOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order repositories by. + field: GistOrderField! +} + +# Properties by which gist connections can be ordered. +enum GistOrderField { + # Order gists by creation time + CREATED_AT + + # Order gists by push time + PUSHED_AT + + # Order gists by update time + UPDATED_AT +} + +# The privacy of a Gist +enum GistPrivacy { + # Gists that are public and secret + ALL + + # Public + PUBLIC + + # Secret + SECRET +} + +# Represents an actor in a Git commit (ie. an author or committer). +type GitActor { + # A URL pointing to the author's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + + # The timestamp of the Git action (authoring or committing). + date: GitTimestamp + + # The email in the Git commit. + email: String + + # The name in the Git commit. + name: String + + # The GitHub user corresponding to the email field. Null if no such user exists. + user: User +} + +# Represents information about the GitHub instance. +type GitHubMetadata { + # Returns a String that's a SHA of `github-services` + gitHubServicesSha: GitObjectID! + + # IP addresses that users connect to for git operations + gitIpAddresses: [String!] + + # IP addresses that service hooks are sent from + hookIpAddresses: [String!] + + # IP addresses that the importer connects from + importerIpAddresses: [String!] + + # Whether or not users are verified + isPasswordAuthenticationVerifiable: Boolean! + + # IP addresses for GitHub Pages' A records + pagesIpAddresses: [String!] +} + +# Represents a Git object. +interface GitObject { + # An abbreviated version of the Git object ID + abbreviatedOid: String! + + # The HTTP path for this Git object + commitResourcePath: URI! + + # The HTTP URL for this Git object + commitUrl: URI! + id: ID! + + # The Git object ID + oid: GitObjectID! + + # The Repository the Git object belongs to + repository: Repository! +} + +# A Git object ID. +scalar GitObjectID + +# Git SSH string +scalar GitSSHRemote + +# Information about a signature (GPG or S/MIME) on a Commit or Tag. +interface GitSignature { + # Email used to sign this object. + email: String! + + # True if the signature is valid and verified by GitHub. + isValid: Boolean! + + # Payload for GPG signing object. Raw ODB object without the signature header. + payload: String! + + # ASCII-armored signature header from object. + signature: String! + + # GitHub user corresponding to the email signing this commit. + signer: User + + # The state of this signature. `VALID` if signature is valid and verified by + # GitHub, otherwise represents reason why signature is considered invalid. + state: GitSignatureState! + + # True if the signature was made with GitHub's signing key. + wasSignedByGitHub: Boolean! +} + +# The state of a Git signature. +enum GitSignatureState { + # The signing certificate or its chain could not be verified + BAD_CERT + + # Invalid email used for signing + BAD_EMAIL + + # Signing key expired + EXPIRED_KEY + + # Internal error - the GPG verification service misbehaved + GPGVERIFY_ERROR + + # Internal error - the GPG verification service is unavailable at the moment + GPGVERIFY_UNAVAILABLE + + # Invalid signature + INVALID + + # Malformed signature + MALFORMED_SIG + + # The usage flags for the key that signed this don't allow signing + NOT_SIGNING_KEY + + # Email used for signing not known to GitHub + NO_USER + + # Valid siganture, though certificate revocation check failed + OCSP_ERROR + + # Valid signature, pending certificate revocation checking + OCSP_PENDING + + # One or more certificates in chain has been revoked + OCSP_REVOKED + + # Key used for signing not known to GitHub + UNKNOWN_KEY + + # Unknown signature type + UNKNOWN_SIG_TYPE + + # Unsigned + UNSIGNED + + # Email used for signing unverified on GitHub + UNVERIFIED_EMAIL + + # Valid signature and verified by GitHub + VALID +} + +# An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. +scalar GitTimestamp + +# Represents a GPG signature on a Commit or Tag. +type GpgSignature implements GitSignature { + # Email used to sign this object. + email: String! + + # True if the signature is valid and verified by GitHub. + isValid: Boolean! + + # Hex-encoded ID of the key that signed this object. + keyId: String + + # Payload for GPG signing object. Raw ODB object without the signature header. + payload: String! + + # ASCII-armored signature header from object. + signature: String! + + # GitHub user corresponding to the email signing this commit. + signer: User + + # The state of this signature. `VALID` if signature is valid and verified by + # GitHub, otherwise represents reason why signature is considered invalid. + state: GitSignatureState! + + # True if the signature was made with GitHub's signing key. + wasSignedByGitHub: Boolean! +} + +# A string containing HTML code. +scalar HTML + +# Represents a 'head_ref_deleted' event on a given pull request. +type HeadRefDeletedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the Ref associated with the `head_ref_deleted` event. + headRef: Ref + + # Identifies the name of the Ref associated with the `head_ref_deleted` event. + headRefName: String! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! +} + +# Represents a 'head_ref_force_pushed' event on a given pull request. +type HeadRefForcePushedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the after commit SHA for the 'head_ref_force_pushed' event. + afterCommit: Commit + + # Identifies the before commit SHA for the 'head_ref_force_pushed' event. + beforeCommit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # Identifies the fully qualified ref name for the 'head_ref_force_pushed' event. + ref: Ref +} + +# Represents a 'head_ref_restored' event on a given pull request. +type HeadRefRestoredEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! +} + +# An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. +type Issue implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + # Reason that the conversation was locked. + activeLockReason: LockReason + + # A list of Users assigned to this object. + assignees( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # Identifies the body of the issue. + body: String! + + # Identifies the body of the issue rendered to HTML. + bodyHTML: HTML! + + # Identifies the body of the issue rendered to text. + bodyText: String! + + # `true` if the object is closed (definition of closed may depend on type) + closed: Boolean! + + # Identifies the date and time when the object was closed. + closedAt: DateTime + + # A list of comments associated with the Issue. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): IssueCommentConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # A list of labels associated with the object. + labels( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): LabelConnection + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # `true` if the object is locked + locked: Boolean! + + # Identifies the milestone associated with the issue. + milestone: Milestone + + # Identifies the issue number. + number: Int! + + # A list of Users that are participating in the Issue conversation. + participants( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # List of project cards associated with this issue. + projectCards( + # Returns the elements in the list that come after the specified cursor. + after: String + + # A list of archived states to filter the cards by + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProjectCardConnection! + + # Identifies when the comment was published at. + publishedAt: DateTime + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # The repository associated with this node. + repository: Repository! + + # The HTTP path for this issue + resourcePath: URI! + + # Identifies the state of the issue. + state: IssueState! + + # A list of events, comments, commits, etc. associated with the issue. + timeline( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows filtering timeline events by a `since` timestamp. + since: DateTime + ): IssueTimelineConnection! + + # Identifies the issue title. + title: String! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this issue + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Can user react to this subject + viewerCanReact: Boolean! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState +} + +# Represents a comment on an Issue. +type IssueComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # The body as Markdown. + body: String! + + # The body rendered to HTML. + bodyHTML: HTML! + + # The body rendered to text. + bodyText: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # Identifies the issue associated with the comment. + issue: Issue! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies when the comment was published at. + publishedAt: DateTime + + # Returns the pull request associated with the comment, if this comment was made on a + # pull request. + pullRequest: PullRequest + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # The repository associated with this node. + repository: Repository! + + # The HTTP path for this issue comment + resourcePath: URI! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this issue comment + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! + + # Can user react to this subject + viewerCanReact: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# The connection type for IssueComment. +type IssueCommentConnection { + # A list of edges. + edges: [IssueCommentEdge] + + # A list of nodes. + nodes: [IssueComment] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type IssueCommentEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: IssueComment +} + +# The connection type for Issue. +type IssueConnection { + # A list of edges. + edges: [IssueEdge] + + # A list of nodes. + nodes: [Issue] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type IssueEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Issue +} + +# Used for return value of Repository.issueOrPullRequest. +union IssueOrPullRequest = Issue | PullRequest + +# Ways in which lists of issues can be ordered upon return. +input IssueOrder { + # The direction in which to order issues by the specified field. + direction: OrderDirection! + + # The field in which to order issues by. + field: IssueOrderField! +} + +# Properties by which issue connections can be ordered. +enum IssueOrderField { + # Order issues by comment count + COMMENTS + + # Order issues by creation time + CREATED_AT + + # Order issues by update time + UPDATED_AT +} + +# The possible PubSub channels for an issue. +enum IssuePubSubTopic { + # The channel ID for marking an issue as read. + MARKASREAD + + # The channel ID for observing issue state updates. + STATE + + # The channel ID for updating items on the issue timeline. + TIMELINE + + # The channel ID for observing issue updates. + UPDATED +} + +# The possible states of an issue. +enum IssueState { + # An issue that has been closed + CLOSED + + # An issue that is still open + OPEN +} + +# The connection type for IssueTimelineItem. +type IssueTimelineConnection { + # A list of edges. + edges: [IssueTimelineItemEdge] + + # A list of nodes. + nodes: [IssueTimelineItem] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An item in an issue timeline +union IssueTimelineItem = AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent | DemilestonedEvent | IssueComment | LabeledEvent | LockedEvent | MilestonedEvent | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent + +# An edge in a connection. +type IssueTimelineItemEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: IssueTimelineItem +} + +# An item in an issue timeline +union IssueTimelineItems = AddedToProjectEvent | AssignedEvent | ClosedEvent | CommentDeletedEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | IssueComment | LabeledEvent | LockedEvent | MentionedEvent | MilestonedEvent | MovedColumnsInProjectEvent | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent + +# An edge in a connection. +type IssueTimelineItemsEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: IssueTimelineItems +} + +# The possible item types found in a timeline. +enum IssueTimelineItemsItemType { + # Represents a 'added_to_project' event on a given issue or pull request. + ADDED_TO_PROJECT_EVENT + + # Represents an 'assigned' event on any assignable object. + ASSIGNED_EVENT + + # Represents a 'closed' event on any `Closable`. + CLOSED_EVENT + + # Represents a 'comment_deleted' event on a given issue or pull request. + COMMENT_DELETED_EVENT + + # Represents a 'converted_note_to_issue' event on a given issue or pull request. + CONVERTED_NOTE_TO_ISSUE_EVENT + + # Represents a mention made by one issue or pull request to another. + CROSS_REFERENCED_EVENT + + # Represents a 'demilestoned' event on a given issue or pull request. + DEMILESTONED_EVENT + + # Represents a comment on an Issue. + ISSUE_COMMENT + + # Represents a 'labeled' event on a given issue or pull request. + LABELED_EVENT + + # Represents a 'locked' event on a given issue or pull request. + LOCKED_EVENT + + # Represents a 'mentioned' event on a given issue or pull request. + MENTIONED_EVENT + + # Represents a 'milestoned' event on a given issue or pull request. + MILESTONED_EVENT + + # Represents a 'moved_columns_in_project' event on a given issue or pull request. + MOVED_COLUMNS_IN_PROJECT_EVENT + + # Represents a 'referenced' event on a given `ReferencedSubject`. + REFERENCED_EVENT + + # Represents a 'removed_from_project' event on a given issue or pull request. + REMOVED_FROM_PROJECT_EVENT + + # Represents a 'renamed' event on a given issue or pull request + RENAMED_TITLE_EVENT + + # Represents a 'reopened' event on any `Closable`. + REOPENED_EVENT + + # Represents a 'subscribed' event on a given `Subscribable`. + SUBSCRIBED_EVENT + + # Represents an 'unassigned' event on any assignable object. + UNASSIGNED_EVENT + + # Represents an 'unlabeled' event on a given issue or pull request. + UNLABELED_EVENT + + # Represents an 'unlocked' event on a given issue or pull request. + UNLOCKED_EVENT + + # Represents an 'unsubscribed' event on a given `Subscribable`. + UNSUBSCRIBED_EVENT +} + +# A label for categorizing Issues or Milestones with a given Repository. +type Label implements Node { + # Identifies the label color. + color: String! + + # Identifies the date and time when the label was created. + createdAt: DateTime + + # A brief description of this label. + description: String + id: ID! + + # Indicates whether or not this is a default label. + isDefault: Boolean! + + # A list of issues associated with this label. + issues( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for issues returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the issues by. + states: [IssueState!] + ): IssueConnection! + + # Identifies the label name. + name: String! + + # A list of pull requests associated with this label. + pullRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # The base ref name to filter the pull requests by. + baseRefName: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # The head ref name to filter the pull requests by. + headRefName: String + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for pull requests returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the pull requests by. + states: [PullRequestState!] + ): PullRequestConnection! + + # The repository associated with this label. + repository: Repository! + + # The HTTP path for this label. + resourcePath: URI! + + # Identifies the date and time when the label was last updated. + updatedAt: DateTime + + # The HTTP URL for this label. + url: URI! +} + +# The connection type for Label. +type LabelConnection { + # A list of edges. + edges: [LabelEdge] + + # A list of nodes. + nodes: [Label] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type LabelEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Label +} + +# An object that can have labels assigned to it. +interface Labelable { + # A list of labels associated with the object. + labels( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): LabelConnection +} + +# Represents a 'labeled' event on a given issue or pull request. +type LabeledEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the label associated with the 'labeled' event. + label: Label! + + # Identifies the `Labelable` associated with the event. + labelable: Labelable! +} + +# Represents a given language found in repositories. +type Language implements Node { + # The color defined for the current language. + color: String + id: ID! + + # The name of the current language. + name: String! +} + +# A list of languages associated with the parent. +type LanguageConnection { + # A list of edges. + edges: [LanguageEdge] + + # A list of nodes. + nodes: [Language] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! + + # The total size in bytes of files written in that language. + totalSize: Int! +} + +# Represents the language of a repository. +type LanguageEdge { + cursor: String! + node: Language! + + # The number of bytes of code written in the language. + size: Int! +} + +# Ordering options for language connections. +input LanguageOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order languages by. + field: LanguageOrderField! +} + +# Properties by which language connections can be ordered. +enum LanguageOrderField { + # Order languages by the size of all files containing the language + SIZE +} + +# A repository's open source license +type License implements Node { + # The full text of the license + body: String! + + # The conditions set by the license + conditions: [LicenseRule]! + + # A human-readable description of the license + description: String + + # Whether the license should be featured + featured: Boolean! + + # Whether the license should be displayed in license pickers + hidden: Boolean! + id: ID! + + # Instructions on how to implement the license + implementation: String + + # The lowercased SPDX ID of the license + key: String! + + # The limitations set by the license + limitations: [LicenseRule]! + + # The license full name specified by + name: String! + + # Customary short name if applicable (e.g, GPLv3) + nickname: String + + # The permissions set by the license + permissions: [LicenseRule]! + + # Whether the license is a pseudo-license placeholder (e.g., other, no-license) + pseudoLicense: Boolean! + + # Short identifier specified by + spdxId: String + + # URL to the license on + url: URI +} + +# Describes a License's conditions, permissions, and limitations +type LicenseRule { + # A description of the rule + description: String! + + # The machine-readable rule key + key: String! + + # The human-readable rule label + label: String! +} + +# Autogenerated input type of LockLockable +input LockLockableInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # A reason for why the issue or pull request will be locked. + lockReason: LockReason + + # ID of the issue or pull request to be locked. + lockableId: ID! +} + +# Autogenerated return type of LockLockable +type LockLockablePayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The item that was locked. + lockedRecord: Lockable +} + +# The possible reasons that an issue or pull request was locked. +enum LockReason { + # The issue or pull request was locked because the conversation was off-topic. + OFF_TOPIC + + # The issue or pull request was locked because the conversation was resolved. + RESOLVED + + # The issue or pull request was locked because the conversation was spam. + SPAM + + # The issue or pull request was locked because the conversation was too heated. + TOO_HEATED +} + +# An object that can be locked. +interface Lockable { + # Reason that the conversation was locked. + activeLockReason: LockReason + + # `true` if the object is locked + locked: Boolean! +} + +# Represents a 'locked' event on a given issue or pull request. +type LockedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Reason that the conversation was locked (optional). + lockReason: LockReason + + # Object that was locked. + lockable: Lockable! +} + +# A public description of a Marketplace category. +type MarketplaceCategory implements Node { + # The category's description. + description: String + + # The technical description of how apps listed in this category work with GitHub. + howItWorks: String + id: ID! + + # The category's name. + name: String! + + # How many Marketplace listings have this as their primary category. + primaryListingCount: Int! + + # The HTTP path for this Marketplace category. + resourcePath: URI! + + # How many Marketplace listings have this as their secondary category. + secondaryListingCount: Int! + + # The short name of the category used in its URL. + slug: String! + + # The HTTP URL for this Marketplace category. + url: URI! +} + +# An edge in a connection. +type MarketplaceCategoryEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: MarketplaceCategory +} + +# A listing in the GitHub integration marketplace. +type MarketplaceListing implements Node { + # The GitHub App this listing represents. + app: App + + # URL to the listing owner's company site. + companyUrl: URI + + # The HTTP path for configuring access to the listing's integration or OAuth app + configurationResourcePath: URI! + + # The HTTP URL for configuring access to the listing's integration or OAuth app + configurationUrl: URI! + + # URL to the listing's documentation. + documentationUrl: URI + + # The listing's detailed description. + extendedDescription: String + + # The listing's detailed description rendered to HTML. + extendedDescriptionHTML: HTML! + + # The listing's introductory description. + fullDescription: String! + + # The listing's introductory description rendered to HTML. + fullDescriptionHTML: HTML! + + # Whether this listing has been submitted for review from GitHub for approval to be displayed in the Marketplace. + hasApprovalBeenRequested: Boolean! + + # Does this listing have any plans with a free trial? + hasPublishedFreeTrialPlans: Boolean! + + # Does this listing have a terms of service link? + hasTermsOfService: Boolean! + + # A technical description of how this app works with GitHub. + howItWorks: String + + # The listing's technical description rendered to HTML. + howItWorksHTML: HTML! + id: ID! + + # URL to install the product to the viewer's account or organization. + installationUrl: URI + + # Whether this listing's app has been installed for the current viewer + installedForViewer: Boolean! + + # Whether this listing has been approved for display in the Marketplace. + isApproved: Boolean! + + # Whether this listing has been removed from the Marketplace. + isDelisted: Boolean! + + # Whether this listing is still an editable draft that has not been submitted + # for review and is not publicly visible in the Marketplace. + isDraft: Boolean! + + # Whether the product this listing represents is available as part of a paid plan. + isPaid: Boolean! + + # Whether this listing has been rejected by GitHub for display in the Marketplace. + isRejected: Boolean! + + # The hex color code, without the leading '#', for the logo background. + logoBackgroundColor: String! + + # URL for the listing's logo image. + logoUrl( + # The size in pixels of the resulting square image. + size: Int = 400 + ): URI + + # The listing's full name. + name: String! + + # The listing's very short description without a trailing period or ampersands. + normalizedShortDescription: String! + + # URL to the listing's detailed pricing. + pricingUrl: URI + + # The category that best describes the listing. + primaryCategory: MarketplaceCategory! + + # URL to the listing's privacy policy. + privacyPolicyUrl: URI! + + # The HTTP path for the Marketplace listing. + resourcePath: URI! + + # The URLs for the listing's screenshots. + screenshotUrls: [String]! + + # An alternate category that describes the listing. + secondaryCategory: MarketplaceCategory + + # The listing's very short description. + shortDescription: String! + + # The short name of the listing used in its URL. + slug: String! + + # URL to the listing's status page. + statusUrl: URI + + # An email address for support for this listing's app. + supportEmail: String + + # Either a URL or an email address for support for this listing's app. + supportUrl: URI! + + # URL to the listing's terms of service. + termsOfServiceUrl: URI + + # The HTTP URL for the Marketplace listing. + url: URI! + + # Can the current viewer add plans for this Marketplace listing. + viewerCanAddPlans: Boolean! + + # Can the current viewer approve this Marketplace listing. + viewerCanApprove: Boolean! + + # Can the current viewer delist this Marketplace listing. + viewerCanDelist: Boolean! + + # Can the current viewer edit this Marketplace listing. + viewerCanEdit: Boolean! + + # Can the current viewer edit the primary and secondary category of this + # Marketplace listing. + viewerCanEditCategories: Boolean! + + # Can the current viewer edit the plans for this Marketplace listing. + viewerCanEditPlans: Boolean! + + # Can the current viewer return this Marketplace listing to draft state + # so it becomes editable again. + viewerCanRedraft: Boolean! + + # Can the current viewer reject this Marketplace listing by returning it to + # an editable draft state or rejecting it entirely. + viewerCanReject: Boolean! + + # Can the current viewer request this listing be reviewed for display in + # the Marketplace. + viewerCanRequestApproval: Boolean! + + # Indicates whether the current user has an active subscription to this Marketplace listing. + viewerHasPurchased: Boolean! + + # Indicates if the current user has purchased a subscription to this Marketplace listing + # for all of the organizations the user owns. + viewerHasPurchasedForAllOrganizations: Boolean! + + # Does the current viewer role allow them to administer this Marketplace listing. + viewerIsListingAdmin: Boolean! +} + +# Look up Marketplace Listings +type MarketplaceListingConnection { + # A list of edges. + edges: [MarketplaceListingEdge] + + # A list of nodes. + nodes: [MarketplaceListing] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type MarketplaceListingEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: MarketplaceListing +} + +# Represents a 'mentioned' event on a given issue or pull request. +type MentionedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# Whether or not a PullRequest can be merged. +enum MergeableState { + # The pull request cannot be merged due to merge conflicts. + CONFLICTING + + # The pull request can be merged. + MERGEABLE + + # The mergeability of the pull request is still being calculated. + UNKNOWN +} + +# Represents a 'merged' event on a given pull request. +type MergedEvent implements Node & UniformResourceLocatable { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the commit associated with the `merge` event. + commit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the Ref associated with the `merge` event. + mergeRef: Ref + + # Identifies the name of the Ref associated with the `merge` event. + mergeRefName: String! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # The HTTP path for this merged event. + resourcePath: URI! + + # The HTTP URL for this merged event. + url: URI! +} + +# Represents a Milestone object on a given repository. +type Milestone implements Closable & Node & UniformResourceLocatable { + # `true` if the object is closed (definition of closed may depend on type) + closed: Boolean! + + # Identifies the date and time when the object was closed. + closedAt: DateTime + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the actor who created the milestone. + creator: Actor + + # Identifies the description of the milestone. + description: String + + # Identifies the due date of the milestone. + dueOn: DateTime + id: ID! + + # A list of issues associated with the milestone. + issues( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for issues returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the issues by. + states: [IssueState!] + ): IssueConnection! + + # Identifies the number of the milestone. + number: Int! + + # A list of pull requests associated with the milestone. + pullRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # The base ref name to filter the pull requests by. + baseRefName: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # The head ref name to filter the pull requests by. + headRefName: String + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for pull requests returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the pull requests by. + states: [PullRequestState!] + ): PullRequestConnection! + + # The repository associated with this milestone. + repository: Repository! + + # The HTTP path for this milestone + resourcePath: URI! + + # Identifies the state of the milestone. + state: MilestoneState! + + # Identifies the title of the milestone. + title: String! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this milestone + url: URI! +} + +# The connection type for Milestone. +type MilestoneConnection { + # A list of edges. + edges: [MilestoneEdge] + + # A list of nodes. + nodes: [Milestone] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type MilestoneEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Milestone +} + +# Types that can be inside a Milestone. +union MilestoneItem = Issue | PullRequest + +# Ordering options for milestone connections. +input MilestoneOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order milestones by. + field: MilestoneOrderField! +} + +# Properties by which milestone connections can be ordered. +enum MilestoneOrderField { + # Order milestones by when they were created. + CREATED_AT + + # Order milestones by when they are due. + DUE_DATE + + # Order milestones by their number. + NUMBER + + # Order milestones by when they were last updated. + UPDATED_AT +} + +# The possible states of a milestone. +enum MilestoneState { + # A milestone that has been closed. + CLOSED + + # A milestone that is still open. + OPEN +} + +# Represents a 'milestoned' event on a given issue or pull request. +type MilestonedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the milestone title associated with the 'milestoned' event. + milestoneTitle: String! + + # Object referenced by event. + subject: MilestoneItem! +} + +# Autogenerated input type of MoveProjectCard +input MoveProjectCardInput { + # Place the new card after the card with this id. Pass null to place it at the top. + afterCardId: ID + + # The id of the card to move. + cardId: ID! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The id of the column to move it into. + columnId: ID! +} + +# Autogenerated return type of MoveProjectCard +type MoveProjectCardPayload { + # The new edge of the moved card. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + cardEdge: ProjectCardEdge! + + # A unique identifier for the client performing the mutation. + clientMutationId: String +} + +# Autogenerated input type of MoveProjectColumn +input MoveProjectColumnInput { + # Place the new column after the column with this id. Pass null to place it at the front. + afterColumnId: ID + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The id of the column to move. + columnId: ID! +} + +# Autogenerated return type of MoveProjectColumn +type MoveProjectColumnPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The new edge of the moved column. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + columnEdge: ProjectColumnEdge! +} + +# Represents a 'moved_columns_in_project' event on a given issue or pull request. +type MovedColumnsInProjectEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# The root query for implementing GraphQL mutations. +type Mutation { + # Applies a suggested topic to the repository. + acceptTopicSuggestion(input: AcceptTopicSuggestionInput!): AcceptTopicSuggestionPayload + + # Adds a comment to an Issue or Pull Request. + addComment(input: AddCommentInput!): AddCommentPayload + + # Adds a card to a ProjectColumn. Either `contentId` or `note` must be provided but **not** both. + addProjectCard(input: AddProjectCardInput!): AddProjectCardPayload + + # Adds a column to a Project. + addProjectColumn(input: AddProjectColumnInput!): AddProjectColumnPayload + + # Adds a review to a Pull Request. + addPullRequestReview(input: AddPullRequestReviewInput!): AddPullRequestReviewPayload + + # Adds a comment to a review. + addPullRequestReviewComment(input: AddPullRequestReviewCommentInput!): AddPullRequestReviewCommentPayload + + # Adds a reaction to a subject. + addReaction(input: AddReactionInput!): AddReactionPayload + + # Adds a star to a Starrable. + addStar(input: AddStarInput!): AddStarPayload + + # Creates a new project. + createProject(input: CreateProjectInput!): CreateProjectPayload + + # Rejects a suggested topic for the repository. + declineTopicSuggestion(input: DeclineTopicSuggestionInput!): DeclineTopicSuggestionPayload + + # Deletes a project. + deleteProject(input: DeleteProjectInput!): DeleteProjectPayload + + # Deletes a project card. + deleteProjectCard(input: DeleteProjectCardInput!): DeleteProjectCardPayload + + # Deletes a project column. + deleteProjectColumn(input: DeleteProjectColumnInput!): DeleteProjectColumnPayload + + # Deletes a pull request review. + deletePullRequestReview(input: DeletePullRequestReviewInput!): DeletePullRequestReviewPayload + + # Dismisses an approved or rejected pull request review. + dismissPullRequestReview(input: DismissPullRequestReviewInput!): DismissPullRequestReviewPayload + + # Lock a lockable object + lockLockable(input: LockLockableInput!): LockLockablePayload + + # Moves a project card to another place. + moveProjectCard(input: MoveProjectCardInput!): MoveProjectCardPayload + + # Moves a project column to another place. + moveProjectColumn(input: MoveProjectColumnInput!): MoveProjectColumnPayload + + # Removes outside collaborator from all repositories in an organization. + removeOutsideCollaborator(input: RemoveOutsideCollaboratorInput!): RemoveOutsideCollaboratorPayload + + # Removes a reaction from a subject. + removeReaction(input: RemoveReactionInput!): RemoveReactionPayload + + # Removes a star from a Starrable. + removeStar(input: RemoveStarInput!): RemoveStarPayload + + # Set review requests on a pull request. + requestReviews(input: RequestReviewsInput!): RequestReviewsPayload + + # Submits a pending pull request review. + submitPullRequestReview(input: SubmitPullRequestReviewInput!): SubmitPullRequestReviewPayload + + # Unlock a lockable object + unlockLockable(input: UnlockLockableInput!): UnlockLockablePayload + + # Updates an existing project. + updateProject(input: UpdateProjectInput!): UpdateProjectPayload + + # Updates an existing project card. + updateProjectCard(input: UpdateProjectCardInput!): UpdateProjectCardPayload + + # Updates an existing project column. + updateProjectColumn(input: UpdateProjectColumnInput!): UpdateProjectColumnPayload + + # Updates the body of a pull request review. + updatePullRequestReview(input: UpdatePullRequestReviewInput!): UpdatePullRequestReviewPayload + + # Updates a pull request review comment. + updatePullRequestReviewComment(input: UpdatePullRequestReviewCommentInput!): UpdatePullRequestReviewCommentPayload + + # Updates the state for subscribable subjects. + updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionPayload + + # Replaces the repository's topics with the given topics. + updateTopics(input: UpdateTopicsInput!): UpdateTopicsPayload +} + +# An object with an ID. +interface Node { + # ID of the object. + id: ID! +} + +# Possible directions in which to order a list of items when provided an `orderBy` argument. +enum OrderDirection { + # Specifies an ascending order for a given `orderBy` argument. + ASC + + # Specifies a descending order for a given `orderBy` argument. + DESC +} + +# An account on GitHub, with one or more owners, that has repositories, members and teams. +type Organization implements Actor & Node & ProjectOwner & RegistryPackageOwner & RegistryPackageSearch & RepositoryOwner & UniformResourceLocatable { + # A URL pointing to the organization's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + + # Identifies the primary key from the database. + databaseId: Int + + # The organization's public profile description. + description: String + + # The organization's public email. + email: String + id: ID! + + # Whether the organization has verified its profile email and website. + isVerified: Boolean! + + # The organization's public profile location. + location: String + + # The organization's login name. + login: String! + + # A list of users who are members of this organization. + members( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # The organization's public profile name. + name: String + + # The HTTP path creating a new team + newTeamResourcePath: URI! + + # The HTTP URL creating a new team + newTeamUrl: URI! + + # The billing email for the organization. + organizationBillingEmail: String + + # A list of repositories this user has pinned to their profile + pinnedRepositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # Find project by number. + project( + # The project number to find. + number: Int! + ): Project + + # A list of projects under the owner. + projects( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for projects returned from the connection + orderBy: ProjectOrder + + # Query to search projects by, currently only searching by name. + search: String + + # A list of states to filter the projects by. + states: [ProjectState!] + ): ProjectConnection! + + # The HTTP path listing organization's projects + projectsResourcePath: URI! + + # The HTTP URL listing organization's projects + projectsUrl: URI! + + # A list of repositories that the user owns. + repositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they are forks of another repository + isFork: Boolean + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # Find Repository. + repository( + # Name of Repository to find. + name: String! + ): Repository + + # When true the organization requires all members, billing managers, and outside + # collaborators to enable two-factor authentication. + requiresTwoFactorAuthentication: Boolean + + # The HTTP path for this organization. + resourcePath: URI! + + # The Organization's SAML Identity Providers + samlIdentityProvider: OrganizationIdentityProvider + + # Find an organization's team by its slug. + team( + # The name or slug of the team to find. + slug: String! + ): Team + + # A list of teams in this organization. + teams( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # If true, filters teams that are mapped to an LDAP Group (Enterprise only) + ldapMapped: Boolean + + # Ordering options for teams returned from the connection + orderBy: TeamOrder + + # If non-null, filters teams according to privacy + privacy: TeamPrivacy + + # If non-null, filters teams with query on team name and team slug + query: String + + # If non-null, filters teams according to whether the viewer is an admin or member on team + role: TeamRole + + # If true, restrict to only root teams + rootTeamsOnly: Boolean = false + + # User logins to filter by + userLogins: [String!] + ): TeamConnection! + + # The HTTP path listing organization's teams + teamsResourcePath: URI! + + # The HTTP URL listing organization's teams + teamsUrl: URI! + + # The HTTP URL for this organization. + url: URI! + + # Organization is adminable by the viewer. + viewerCanAdminister: Boolean! + + # Can the current viewer create new projects on this owner. + viewerCanCreateProjects: Boolean! + + # Viewer can create repositories on this organization + viewerCanCreateRepositories: Boolean! + + # Viewer can create teams on this organization. + viewerCanCreateTeams: Boolean! + + # Viewer is an active member of this organization. + viewerIsAMember: Boolean! + + # The organization's public profile URL. + websiteUrl: URI +} + +# The connection type for Organization. +type OrganizationConnection { + # A list of edges. + edges: [OrganizationEdge] + + # A list of nodes. + nodes: [Organization] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type OrganizationEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Organization +} + +# An Identity Provider configured to provision SAML and SCIM identities for Organizations +type OrganizationIdentityProvider implements Node { + # The digest algorithm used to sign SAML requests for the Identity Provider. + digestMethod: URI + + # External Identities provisioned by this Identity Provider + externalIdentities( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ExternalIdentityConnection! + id: ID! + + # The x509 certificate used by the Identity Provder to sign assertions and responses. + idpCertificate: X509Certificate + + # The Issuer Entity ID for the SAML Identity Provider + issuer: String + + # Organization this Identity Provider belongs to + organization: Organization + + # The signature algorithm used to sign SAML requests for the Identity Provider. + signatureMethod: URI + + # The URL endpoint for the Identity Provider's SAML SSO. + ssoUrl: URI +} + +# An Invitation for a user to an organization. +type OrganizationInvitation implements Node { + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The email address of the user invited to the organization. + email: String + id: ID! + + # The type of invitation that was sent (e.g. email, user). + invitationType: OrganizationInvitationType! + + # The user who was invited to the organization. + invitee: User + + # The user who created the invitation. + inviter: User! + + # The organization the invite is for + organization: Organization! + + # The user's pending role in the organization (e.g. member, owner). + role: OrganizationInvitationRole! +} + +# The connection type for OrganizationInvitation. +type OrganizationInvitationConnection { + # A list of edges. + edges: [OrganizationInvitationEdge] + + # A list of nodes. + nodes: [OrganizationInvitation] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type OrganizationInvitationEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: OrganizationInvitation +} + +# The possible organization invitation roles. +enum OrganizationInvitationRole { + # The user is invited to be an admin of the organization. + ADMIN + + # The user is invited to be a billing manager of the organization. + BILLING_MANAGER + + # The user is invited to be a direct member of the organization. + DIRECT_MEMBER + + # The user's previous role will be reinstated. + REINSTATE +} + +# The possible organization invitation types. +enum OrganizationInvitationType { + # The invitation was to an email address. + EMAIL + + # The invitation was to an existing user. + USER +} + +# Information about pagination in a connection. +type PageInfo { + # When paginating forwards, the cursor to continue. + endCursor: String + + # When paginating forwards, are there more items? + hasNextPage: Boolean! + + # When paginating backwards, are there more items? + hasPreviousPage: Boolean! + + # When paginating backwards, the cursor to continue. + startCursor: String +} + +# Projects manage issues, pull requests and notes within a project owner. +type Project implements Closable & Node & Updatable { + # The project's description body. + body: String + + # The projects description body rendered to HTML. + bodyHTML: HTML! + + # `true` if the object is closed (definition of closed may depend on type) + closed: Boolean! + + # Identifies the date and time when the object was closed. + closedAt: DateTime + + # List of columns in the project + columns( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProjectColumnConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The actor who originally created the project. + creator: Actor + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # The project's name. + name: String! + + # The project's number. + number: Int! + + # The project's owner. Currently limited to repositories and organizations. + owner: ProjectOwner! + + # List of pending cards in this project + pendingCards( + # Returns the elements in the list that come after the specified cursor. + after: String + + # A list of archived states to filter the cards by + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProjectCardConnection! + + # The HTTP path for this project + resourcePath: URI! + + # Whether the project is open or closed. + state: ProjectState! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this project + url: URI! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! +} + +# A card in a project. +type ProjectCard implements Node { + # The project column this card is associated under. A card may only belong to one + # project column at a time. The column field will be null if the card is created + # in a pending state and has yet to be associated with a column. Once cards are + # associated with a column, they will not become pending in the future. + column: ProjectColumn + + # The card content item + content: ProjectCardItem + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The actor who created this card + creator: Actor + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # Whether the card is archived + isArchived: Boolean! + + # The card note + note: String + + # The project that contains this card. + project: Project! + + # The HTTP path for this card + resourcePath: URI! + + # The state of ProjectCard + state: ProjectCardState + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this card + url: URI! +} + +# The possible archived states of a project card. +enum ProjectCardArchivedState { + # A project card that is archived + ARCHIVED + + # A project card that is not archived + NOT_ARCHIVED +} + +# The connection type for ProjectCard. +type ProjectCardConnection { + # A list of edges. + edges: [ProjectCardEdge] + + # A list of nodes. + nodes: [ProjectCard] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ProjectCardEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ProjectCard +} + +# Types that can be inside Project Cards. +union ProjectCardItem = Issue | PullRequest + +# Various content states of a ProjectCard +enum ProjectCardState { + # The card has content only. + CONTENT_ONLY + + # The card has a note only. + NOTE_ONLY + + # The card is redacted. + REDACTED +} + +# A column inside a project. +type ProjectColumn implements Node { + # List of cards in the column + cards( + # Returns the elements in the list that come after the specified cursor. + after: String + + # A list of archived states to filter the cards by + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProjectCardConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # The project column's name. + name: String! + + # The project that contains this column. + project: Project! + + # The semantic purpose of the column + purpose: ProjectColumnPurpose + + # The HTTP path for this project column + resourcePath: URI! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this project column + url: URI! +} + +# The connection type for ProjectColumn. +type ProjectColumnConnection { + # A list of edges. + edges: [ProjectColumnEdge] + + # A list of nodes. + nodes: [ProjectColumn] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ProjectColumnEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ProjectColumn +} + +# The semantic purpose of the column - todo, in progress, or done. +enum ProjectColumnPurpose { + # The column contains cards which are complete + DONE + + # The column contains cards which are currently being worked on + IN_PROGRESS + + # The column contains cards still to be worked on + TODO +} + +# A list of projects associated with the owner. +type ProjectConnection { + # A list of edges. + edges: [ProjectEdge] + + # A list of nodes. + nodes: [Project] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ProjectEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Project +} + +# Ways in which lists of projects can be ordered upon return. +input ProjectOrder { + # The direction in which to order projects by the specified field. + direction: OrderDirection! + + # The field in which to order projects by. + field: ProjectOrderField! +} + +# Properties by which project connections can be ordered. +enum ProjectOrderField { + # Order projects by creation time + CREATED_AT + + # Order projects by name + NAME + + # Order projects by update time + UPDATED_AT +} + +# Represents an owner of a Project. +interface ProjectOwner { + id: ID! + + # Find project by number. + project( + # The project number to find. + number: Int! + ): Project + + # A list of projects under the owner. + projects( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for projects returned from the connection + orderBy: ProjectOrder + + # Query to search projects by, currently only searching by name. + search: String + + # A list of states to filter the projects by. + states: [ProjectState!] + ): ProjectConnection! + + # The HTTP path listing owners projects + projectsResourcePath: URI! + + # The HTTP URL listing owners projects + projectsUrl: URI! + + # Can the current viewer create new projects on this owner. + viewerCanCreateProjects: Boolean! +} + +# State of the project; either 'open' or 'closed' +enum ProjectState { + # The project is closed. + CLOSED + + # The project is open. + OPEN +} + +# A repository protected branch. +type ProtectedBranch implements Node { + # The actor who created this protected branch. + creator: Actor + + # Will new commits pushed to this branch dismiss pull request review approvals. + hasDismissableStaleReviews: Boolean! + + # Are reviews required to update this branch. + hasRequiredReviews: Boolean! + + # Are status checks required to update this branch. + hasRequiredStatusChecks: Boolean! + + # Is pushing to this branch restricted. + hasRestrictedPushes: Boolean! + + # Is dismissal of pull request reviews restricted. + hasRestrictedReviewDismissals: Boolean! + + # Are branches required to be up to date before merging. + hasStrictRequiredStatusChecks: Boolean! + id: ID! + + # Can admins overwrite branch protection. + isAdminEnforced: Boolean! + + # The name of the protected branch rule. + name: String! + + # A list push allowances for this protected branch. + pushAllowances( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): PushAllowanceConnection! + + # The repository associated with this protected branch. + repository: Repository! + + # List of required status check contexts that must pass for commits to be accepted to this branch. + requiredStatusCheckContexts: [String] + + # A list review dismissal allowances for this protected branch. + reviewDismissalAllowances( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ReviewDismissalAllowanceConnection! +} + +# The connection type for ProtectedBranch. +type ProtectedBranchConnection { + # A list of edges. + edges: [ProtectedBranchEdge] + + # A list of nodes. + nodes: [ProtectedBranch] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ProtectedBranchEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ProtectedBranch +} + +# A user's public key. +type PublicKey implements Node { + id: ID! + + # The public key string + key: String! +} + +# The connection type for PublicKey. +type PublicKeyConnection { + # A list of edges. + edges: [PublicKeyEdge] + + # A list of nodes. + nodes: [PublicKey] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PublicKeyEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PublicKey +} + +# A repository pull request. +type PullRequest implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { + # Reason that the conversation was locked. + activeLockReason: LockReason + + # The number of additions in this pull request. + additions: Int! + + # A list of Users assigned to this object. + assignees( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # Identifies the base Ref associated with the pull request. + baseRef: Ref + + # Identifies the name of the base Ref associated with the pull request, even if the ref has been deleted. + baseRefName: String! + + # Identifies the oid of the base ref associated with the pull request, even if the ref has been deleted. + baseRefOid: GitObjectID! + + # The body as Markdown. + body: String! + + # The body rendered to HTML. + bodyHTML: HTML! + + # The body rendered to text. + bodyText: String! + + # The number of changed files in this pull request. + changedFiles: Int! + + # `true` if the pull request is closed + closed: Boolean! + + # Identifies the date and time when the object was closed. + closedAt: DateTime + + # A list of comments associated with the pull request. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): IssueCommentConnection! + + # A list of commits present in this pull request's head branch not present in the base branch. + commits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): PullRequestCommitConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The number of deletions in this pull request. + deletions: Int! + + # The actor who edited this pull request's body. + editor: Actor + + # Identifies the head Ref associated with the pull request. + headRef: Ref + + # Identifies the name of the head Ref associated with the pull request, even if the ref has been deleted. + headRefName: String! + + # Identifies the oid of the head ref associated with the pull request, even if the ref has been deleted. + headRefOid: GitObjectID! + + # The repository associated with this pull request's head Ref. + headRepository: Repository + + # The owner of the repository associated with this pull request's head Ref. + headRepositoryOwner: RepositoryOwner + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The head and base repositories are different. + isCrossRepository: Boolean! + + # A list of labels associated with the object. + labels( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): LabelConnection + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # `true` if the pull request is locked + locked: Boolean! + + # Indicates whether maintainers can modify the pull request. + maintainerCanModify: Boolean! + + # The commit that was created when this pull request was merged. + mergeCommit: Commit + + # Whether or not the pull request can be merged based on the existence of merge conflicts. + mergeable: MergeableState! + + # Whether or not the pull request was merged. + merged: Boolean! + + # The date and time that the pull request was merged. + mergedAt: DateTime + + # The actor who merged the pull request. + mergedBy: Actor + + # Identifies the milestone associated with the pull request. + milestone: Milestone + + # Identifies the pull request number. + number: Int! + + # A list of Users that are participating in the Pull Request conversation. + participants( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # The permalink to the pull request. + permalink: URI! + + # The commit that GitHub automatically generated to test if this pull request + # could be merged. This field will not return a value if the pull request is + # merged, or if the test merge commit is still being generated. See the + # `mergeable` field for more details on the mergeability of the pull request. + potentialMergeCommit: Commit + + # List of project cards associated with this pull request. + projectCards( + # Returns the elements in the list that come after the specified cursor. + after: String + + # A list of archived states to filter the cards by + archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProjectCardConnection! + + # Identifies when the comment was published at. + publishedAt: DateTime + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # The repository associated with this node. + repository: Repository! + + # The HTTP path for this pull request. + resourcePath: URI! + + # The HTTP path for reverting this pull request. + revertResourcePath: URI! + + # The HTTP URL for reverting this pull request. + revertUrl: URI! + + # A list of review requests associated with the pull request. + reviewRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ReviewRequestConnection + + # A list of reviews associated with the pull request. + reviews( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Filter by author of the review. + author: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # A list of states to filter the reviews. + states: [PullRequestReviewState!] + ): PullRequestReviewConnection + + # Identifies the state of the pull request. + state: PullRequestState! + + # A list of reviewer suggestions based on commit history and past review comments. + suggestedReviewers: [SuggestedReviewer]! + + # A list of events, comments, commits, etc. associated with the pull request. + timeline( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows filtering timeline events by a `since` timestamp. + since: DateTime + ): PullRequestTimelineConnection! + + # Identifies the pull request title. + title: String! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this pull request. + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Whether or not the viewer can apply suggestion. + viewerCanApplySuggestion: Boolean! + + # Can user react to this subject + viewerCanReact: Boolean! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState +} + +# Represents a Git commit part of a pull request. +type PullRequestCommit implements Node & UniformResourceLocatable { + # The Git commit object + commit: Commit! + id: ID! + + # The pull request this commit belongs to + pullRequest: PullRequest! + + # The HTTP path for this pull request commit + resourcePath: URI! + + # The HTTP URL for this pull request commit + url: URI! +} + +# The connection type for PullRequestCommit. +type PullRequestCommitConnection { + # A list of edges. + edges: [PullRequestCommitEdge] + + # A list of nodes. + nodes: [PullRequestCommit] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PullRequestCommitEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequestCommit +} + +# The connection type for PullRequest. +type PullRequestConnection { + # A list of edges. + edges: [PullRequestEdge] + + # A list of nodes. + nodes: [PullRequest] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PullRequestEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequest +} + +# Ways in which lists of issues can be ordered upon return. +input PullRequestOrder { + # The direction in which to order pull requests by the specified field. + direction: OrderDirection! + + # The field in which to order pull requests by. + field: PullRequestOrderField! +} + +# Properties by which pull_requests connections can be ordered. +enum PullRequestOrderField { + # Order pull_requests by creation time + CREATED_AT + + # Order pull_requests by update time + UPDATED_AT +} + +# The possible PubSub channels for a pull request. +enum PullRequestPubSubTopic { + # The channel ID for observing head ref updates. + HEAD_REF + + # The channel ID for marking an pull request as read. + MARKASREAD + + # The channel ID for observing pull request state updates. + STATE + + # The channel ID for updating items on the pull request timeline. + TIMELINE + + # The channel ID for observing pull request updates. + UPDATED +} + +# A review object for a given pull request. +type PullRequestReview implements Comment & Deletable & Node & RepositoryNode & Updatable & UpdatableComment { + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # Identifies the pull request review body. + body: String! + + # The body of this review rendered to HTML. + bodyHTML: HTML! + + # The body of this review rendered as plain text. + bodyText: String! + + # A list of review comments for the current pull request review. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): PullRequestReviewCommentConnection! + + # Identifies the commit associated with this pull request review. + commit: Commit + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies when the comment was published at. + publishedAt: DateTime + + # Identifies the pull request associated with this pull request review. + pullRequest: PullRequest! + + # The repository associated with this node. + repository: Repository! + + # The HTTP path permalink for this PullRequestReview. + resourcePath: URI! + + # Identifies the current state of the pull request review. + state: PullRequestReviewState! + + # Identifies when the Pull Request Review was submitted + submittedAt: DateTime + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL permalink for this PullRequestReview. + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# A review comment associated with a given repository pull request. +type PullRequestReviewComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { + # The actor who authored the comment. + author: Actor + + # Author's association with the subject of the comment. + authorAssociation: CommentAuthorAssociation! + + # The comment body of this review comment. + body: String! + + # The comment body of this review comment rendered to HTML. + bodyHTML: HTML! + + # The comment body of this review comment rendered as plain text. + bodyText: String! + + # Identifies the commit associated with the comment. + commit: Commit! + + # Identifies when the comment was created. + createdAt: DateTime! + + # Check if this comment was created via an email reply. + createdViaEmail: Boolean! + + # Identifies the primary key from the database. + databaseId: Int + + # The diff hunk to which the comment applies. + diffHunk: String! + + # Identifies when the comment was created in a draft state. + draftedAt: DateTime! + + # The actor who edited the comment. + editor: Actor + id: ID! + + # Check if this comment was edited and includes an edit with the creation data + includesCreatedEdit: Boolean! + + # The moment the editor made the last edit + lastEditedAt: DateTime + + # Identifies the original commit associated with the comment. + originalCommit: Commit + + # The original line index in the diff to which the comment applies. + originalPosition: Int! + + # The path to which the comment applies. + path: String! + + # The line index in the diff to which the comment applies. + position: Int + + # Identifies when the comment was published at. + publishedAt: DateTime + + # The pull request associated with this review comment. + pullRequest: PullRequest! + + # The pull request review associated with this review comment. + pullRequestReview: PullRequestReview + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # The comment this is a reply to. + replyTo: PullRequestReviewComment + + # The repository associated with this node. + repository: Repository! + + # The HTTP path permalink for this review comment. + resourcePath: URI! + + # Identifies when the comment was last updated. + updatedAt: DateTime! + + # The HTTP URL permalink for this review comment. + url: URI! + + # A list of edits to this content. + userContentEdits( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserContentEditConnection + + # Check if the current viewer can delete this object. + viewerCanDelete: Boolean! + + # Can user react to this subject + viewerCanReact: Boolean! + + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! + + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! + + # Did the viewer author this comment. + viewerDidAuthor: Boolean! +} + +# The connection type for PullRequestReviewComment. +type PullRequestReviewCommentConnection { + # A list of edges. + edges: [PullRequestReviewCommentEdge] + + # A list of nodes. + nodes: [PullRequestReviewComment] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PullRequestReviewCommentEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequestReviewComment +} + +# The connection type for PullRequestReview. +type PullRequestReviewConnection { + # A list of edges. + edges: [PullRequestReviewEdge] + + # A list of nodes. + nodes: [PullRequestReview] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PullRequestReviewEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequestReview +} + +# The possible events to perform on a pull request review. +enum PullRequestReviewEvent { + # Submit feedback and approve merging these changes. + APPROVE + + # Submit general feedback without explicit approval. + COMMENT + + # Dismiss review so it now longer effects merging. + DISMISS + + # Submit feedback that must be addressed before merging. + REQUEST_CHANGES +} + +# The possible states of a pull request review. +enum PullRequestReviewState { + # A review allowing the pull request to merge. + APPROVED + + # A review blocking the pull request from merging. + CHANGES_REQUESTED + + # An informational review. + COMMENTED + + # A review that has been dismissed. + DISMISSED + + # A review that has not yet been submitted. + PENDING +} + +# A threaded list of comments for a given pull request. +type PullRequestReviewThread implements Node { + # A list of pull request comments associated with the thread. + comments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): PullRequestReviewCommentConnection! + id: ID! + + # Identifies the pull request associated with this thread. + pullRequest: PullRequest! + + # Identifies the repository associated with this thread. + repository: Repository! +} + +# The possible states of a pull request. +enum PullRequestState { + # A pull request that has been closed without being merged. + CLOSED + + # A pull request that has been closed by being merged. + MERGED + + # A pull request that is still open. + OPEN +} + +# The connection type for PullRequestTimelineItem. +type PullRequestTimelineConnection { + # A list of edges. + edges: [PullRequestTimelineItemEdge] + + # A list of nodes. + nodes: [PullRequestTimelineItem] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An item in an pull request timeline +union PullRequestTimelineItem = AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit | CommitCommentThread | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MergedEvent | MilestonedEvent | PullRequestReview | PullRequestReviewComment | PullRequestReviewThread | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent + +# An edge in a connection. +type PullRequestTimelineItemEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequestTimelineItem +} + +# An item in a pull request timeline +union PullRequestTimelineItems = AddedToProjectEvent | AssignedEvent | BaseRefChangedEvent | BaseRefForcePushedEvent | ClosedEvent | CommentDeletedEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MentionedEvent | MergedEvent | MilestonedEvent | MovedColumnsInProjectEvent | PullRequestCommit | PullRequestReview | PullRequestReviewThread | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent + +# An edge in a connection. +type PullRequestTimelineItemsEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PullRequestTimelineItems +} + +# The possible item types found in a timeline. +enum PullRequestTimelineItemsItemType { + # Represents a 'added_to_project' event on a given issue or pull request. + ADDED_TO_PROJECT_EVENT + + # Represents an 'assigned' event on any assignable object. + ASSIGNED_EVENT + + # Represents a 'base_ref_changed' event on a given issue or pull request. + BASE_REF_CHANGED_EVENT + + # Represents a 'base_ref_force_pushed' event on a given pull request. + BASE_REF_FORCE_PUSHED_EVENT + + # Represents a 'closed' event on any `Closable`. + CLOSED_EVENT + + # Represents a 'comment_deleted' event on a given issue or pull request. + COMMENT_DELETED_EVENT + + # Represents a 'converted_note_to_issue' event on a given issue or pull request. + CONVERTED_NOTE_TO_ISSUE_EVENT + + # Represents a mention made by one issue or pull request to another. + CROSS_REFERENCED_EVENT + + # Represents a 'demilestoned' event on a given issue or pull request. + DEMILESTONED_EVENT + + # Represents a 'deployed' event on a given pull request. + DEPLOYED_EVENT + + # Represents a 'deployment_environment_changed' event on a given pull request. + DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT + + # Represents a 'head_ref_deleted' event on a given pull request. + HEAD_REF_DELETED_EVENT + + # Represents a 'head_ref_force_pushed' event on a given pull request. + HEAD_REF_FORCE_PUSHED_EVENT + + # Represents a 'head_ref_restored' event on a given pull request. + HEAD_REF_RESTORED_EVENT + + # Represents a comment on an Issue. + ISSUE_COMMENT + + # Represents a 'labeled' event on a given issue or pull request. + LABELED_EVENT + + # Represents a 'locked' event on a given issue or pull request. + LOCKED_EVENT + + # Represents a 'mentioned' event on a given issue or pull request. + MENTIONED_EVENT + + # Represents a 'merged' event on a given pull request. + MERGED_EVENT + + # Represents a 'milestoned' event on a given issue or pull request. + MILESTONED_EVENT + + # Represents a 'moved_columns_in_project' event on a given issue or pull request. + MOVED_COLUMNS_IN_PROJECT_EVENT + + # Represents a Git commit part of a pull request. + PULL_REQUEST_COMMIT + + # Represents a commit comment thread part of a pull request. + PULL_REQUEST_COMMIT_COMMENT_THREAD + + # A review object for a given pull request. + PULL_REQUEST_REVIEW + + # A threaded list of comments for a given pull request. + PULL_REQUEST_REVIEW_THREAD + + # Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. + PULL_REQUEST_REVISION_MARKER + + # Represents a 'referenced' event on a given `ReferencedSubject`. + REFERENCED_EVENT + + # Represents a 'removed_from_project' event on a given issue or pull request. + REMOVED_FROM_PROJECT_EVENT + + # Represents a 'renamed' event on a given issue or pull request + RENAMED_TITLE_EVENT + + # Represents a 'reopened' event on any `Closable`. + REOPENED_EVENT + + # Represents a 'review_dismissed' event on a given issue or pull request. + REVIEW_DISMISSED_EVENT + + # Represents an 'review_requested' event on a given pull request. + REVIEW_REQUESTED_EVENT + + # Represents an 'review_request_removed' event on a given pull request. + REVIEW_REQUEST_REMOVED_EVENT + + # Represents a 'subscribed' event on a given `Subscribable`. + SUBSCRIBED_EVENT + + # Represents an 'unassigned' event on any assignable object. + UNASSIGNED_EVENT + + # Represents an 'unlabeled' event on a given issue or pull request. + UNLABELED_EVENT + + # Represents an 'unlocked' event on a given issue or pull request. + UNLOCKED_EVENT + + # Represents an 'unsubscribed' event on a given `Subscribable`. + UNSUBSCRIBED_EVENT +} + +# A team or user who has the ability to push to a protected branch. +type PushAllowance implements Node { + # The actor that can push. + actor: PushAllowanceActor + id: ID! + + # Identifies the protected branch associated with the allowed user or team. + protectedBranch: ProtectedBranch! +} + +# Types that can be an actor. +union PushAllowanceActor = Team | User + +# The connection type for PushAllowance. +type PushAllowanceConnection { + # A list of edges. + edges: [PushAllowanceEdge] + + # A list of nodes. + nodes: [PushAllowance] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type PushAllowanceEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: PushAllowance +} + +# The query root of GitHub's GraphQL interface. +type Query { + # Look up a code of conduct by its key + codeOfConduct( + # The code of conduct's key + key: String! + ): CodeOfConduct + + # Look up a code of conduct by its key + codesOfConduct: [CodeOfConduct] + + # Look up an open source license by its key + license( + # The license's downcased SPDX ID + key: String! + ): License + + # Return a list of known open source licenses + licenses: [License]! + + # Get alphabetically sorted list of Marketplace categories + marketplaceCategories( + # Exclude categories with no listings. + excludeEmpty: Boolean + + # Exclude subcategories + excludeSubcategories: Boolean + + # Return only the specified categories. + includeCategories: [String!] + ): [MarketplaceCategory!]! + + # Look up a Marketplace category by its slug. + marketplaceCategory( + # The URL slug of the category. + slug: String! + + # Also check topic aliases for the category slug + useTopicAliases: Boolean + ): MarketplaceCategory + + # Look up a single Marketplace listing + marketplaceListing( + # Select the listing that matches this slug. It's the short name of the listing used in its URL. + slug: String! + ): MarketplaceListing + + # Look up Marketplace listings + marketplaceListings( + # Select listings that can be administered by the specified user. + adminId: ID + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Select listings visible to the viewer even if they are not approved. If omitted or + # false, only approved listings will be returned. + allStates: Boolean + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Select only listings with the given category. + categorySlug: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Select listings for products owned by the specified organization. + organizationId: ID + + # Select only listings where the primary category matches the given category slug. + primaryCategoryOnly: Boolean = false + + # Select the listings with these slugs, if they are visible to the viewer. + slugs: [String] + + # Also check topic aliases for the category slug + useTopicAliases: Boolean + + # Select listings to which user has admin access. If omitted, listings visible to the + # viewer are returned. + viewerCanAdmin: Boolean + + # Select only listings that offer a free trial. + withFreeTrialsOnly: Boolean = false + ): MarketplaceListingConnection! + + # Return information about the GitHub instance + meta: GitHubMetadata! + + # Fetches an object given its ID. + node( + # ID of the object. + id: ID! + ): Node + + # Lookup nodes by a list of IDs. + nodes( + # The list of node IDs. + ids: [ID!]! + ): [Node]! + + # Lookup a organization by login. + organization( + # The organization's login. + login: String! + ): Organization + + # The client's rate limit information. + rateLimit( + # If true, calculate the cost for the query without evaluating it + dryRun: Boolean = false + ): RateLimit + + # Hack to workaround https://github.com/facebook/relay/issues/112 re-exposing the root query object + relay: Query! + + # Lookup a given repository by the owner and repository name. + repository( + # The name of the repository + name: String! + + # The login field of a user or organization + owner: String! + ): Repository + + # Lookup a repository owner (ie. either a User or an Organization) by login. + repositoryOwner( + # The username to lookup the owner by. + login: String! + ): RepositoryOwner + + # Lookup resource by a URL. + resource( + # The URL. + url: URI! + ): UniformResourceLocatable + + # Perform a search across resources. + search( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # The search string to look for. + query: String! + + # The types of search items to search within. + type: SearchType! + ): SearchResultItemConnection! + + # Look up a topic by name. + topic( + # The topic's name. + name: String! + ): Topic + + # Lookup a user by login. + user( + # The user's login. + login: String! + ): User + + # The currently authenticated user. + viewer: User! +} + +# Represents the client's rate limit. +type RateLimit { + # The point cost for the current query counting against the rate limit. + cost: Int! + + # The maximum number of points the client is permitted to consume in a 60 minute window. + limit: Int! + + # The maximum number of nodes this query may return + nodeCount: Int! + + # The number of points remaining in the current rate limit window. + remaining: Int! + + # The time at which the current rate limit window resets in UTC epoch seconds. + resetAt: DateTime! +} + +# Represents a subject that can be reacted on. +interface Reactable { + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # A list of reactions grouped by content left on the subject. + reactionGroups: [ReactionGroup!] + + # A list of Reactions left on the Issue. + reactions( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Allows filtering Reactions by emoji. + content: ReactionContent + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Allows specifying the order in which reactions are returned. + orderBy: ReactionOrder + ): ReactionConnection! + + # Can user react to this subject + viewerCanReact: Boolean! +} + +# The connection type for User. +type ReactingUserConnection { + # A list of edges. + edges: [ReactingUserEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a user that's made a reaction. +type ReactingUserEdge { + # A cursor for use in pagination. + cursor: String! + node: User! + + # The moment when the user made the reaction. + reactedAt: DateTime! +} + +# An emoji reaction to a particular piece of content. +type Reaction implements Node { + # Identifies the emoji reaction. + content: ReactionContent! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # The reactable piece of content + reactable: Reactable! + + # Identifies the user who created this reaction. + user: User +} + +# A list of reactions that have been left on the subject. +type ReactionConnection { + # A list of edges. + edges: [ReactionEdge] + + # A list of nodes. + nodes: [Reaction] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! + + # Whether or not the authenticated user has left a reaction on the subject. + viewerHasReacted: Boolean! +} + +# Emojis that can be attached to Issues, Pull Requests and Comments. +enum ReactionContent { + # Represents the 😕 emoji. + CONFUSED + + # Represents the ❤️ emoji. + HEART + + # Represents the 🎉 emoji. + HOORAY + + # Represents the 😄 emoji. + LAUGH + + # Represents the 👎 emoji. + THUMBS_DOWN + + # Represents the 👍 emoji. + THUMBS_UP +} + +# An edge in a connection. +type ReactionEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Reaction +} + +# A group of emoji reactions to a particular piece of content. +type ReactionGroup { + # Identifies the emoji reaction. + content: ReactionContent! + + # Identifies when the reaction was created. + createdAt: DateTime + + # The subject that was reacted to. + subject: Reactable! + + # Users who have reacted to the reaction subject with the emotion represented by this reaction group + users( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ReactingUserConnection! + + # Whether or not the authenticated user has left a reaction on the subject. + viewerHasReacted: Boolean! +} + +# Ways in which lists of reactions can be ordered upon return. +input ReactionOrder { + # The direction in which to order reactions by the specified field. + direction: OrderDirection! + + # The field in which to order reactions by. + field: ReactionOrderField! +} + +# A list of fields that reactions can be ordered by. +enum ReactionOrderField { + # Allows ordering a list of reactions by when they were created. + CREATED_AT +} + +# Represents a Git reference. +type Ref implements Node { + # A list of pull requests with this ref as the head ref. + associatedPullRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # The base ref name to filter the pull requests by. + baseRefName: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # The head ref name to filter the pull requests by. + headRefName: String + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for pull requests returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the pull requests by. + states: [PullRequestState!] + ): PullRequestConnection! + id: ID! + + # The ref name. + name: String! + + # The ref's prefix, such as `refs/heads/` or `refs/tags/`. + prefix: String! + + # The repository the ref belongs to. + repository: Repository! + + # The object the ref points to. + target: GitObject! +} + +# The connection type for Ref. +type RefConnection { + # A list of edges. + edges: [RefEdge] + + # A list of nodes. + nodes: [Ref] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type RefEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Ref +} + +# Ways in which lists of git refs can be ordered upon return. +input RefOrder { + # The direction in which to order refs by the specified field. + direction: OrderDirection! + + # The field in which to order refs by. + field: RefOrderField! +} + +# Properties by which ref connections can be ordered. +enum RefOrderField { + # Order refs by their alphanumeric name + ALPHABETICAL + + # Order refs by underlying commit date if the ref prefix is refs/tags/ + TAG_COMMIT_DATE +} + +# Represents a 'referenced' event on a given `ReferencedSubject`. +type ReferencedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the commit associated with the 'referenced' event. + commit: Commit + + # Identifies the repository associated with the 'referenced' event. + commitRepository: Repository! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Reference originated in a different repository. + isCrossRepository: Boolean! + + # Checks if the commit message itself references the subject. Can be false in the case of a commit comment reference. + isDirectReference: Boolean! + + # Object referenced by event. + subject: ReferencedSubject! +} + +# Any referencable object +union ReferencedSubject = Issue | PullRequest + +# Represents an owner of a registry package. +interface RegistryPackageOwner { + id: ID! +} + +# Represents an interface to search packages on an object. +interface RegistryPackageSearch { + id: ID! +} + +# A release contains the content for a release. +type Release implements Node & UniformResourceLocatable { + # The author of the release + author: User + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the description of the release. + description: String + id: ID! + + # Whether or not the release is a draft + isDraft: Boolean! + + # Whether or not the release is a prerelease + isPrerelease: Boolean! + + # Identifies the title of the release. + name: String + + # Identifies the date and time when the release was created. + publishedAt: DateTime + + # List of releases assets which are dependent on this release. + releaseAssets( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # A list of names to filter the assets by. + name: String + ): ReleaseAssetConnection! + + # The HTTP path for this issue + resourcePath: URI! + + # The Git tag the release points to + tag: Ref + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this issue + url: URI! +} + +# A release asset contains the content for a release asset. +type ReleaseAsset implements Node { + # The asset's content-type + contentType: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The number of times this asset was downloaded + downloadCount: Int! + + # Identifies the URL where you can download the release asset via the browser. + downloadUrl: URI! + id: ID! + + # Identifies the title of the release asset. + name: String! + + # Release that the asset is associated with + release: Release + + # The size (in bytes) of the asset + size: Int! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The user that performed the upload + uploadedBy: User! + + # Identifies the URL of the release asset. + url: URI! +} + +# The connection type for ReleaseAsset. +type ReleaseAssetConnection { + # A list of edges. + edges: [ReleaseAssetEdge] + + # A list of nodes. + nodes: [ReleaseAsset] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ReleaseAssetEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ReleaseAsset +} + +# The connection type for Release. +type ReleaseConnection { + # A list of edges. + edges: [ReleaseEdge] + + # A list of nodes. + nodes: [Release] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ReleaseEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Release +} + +# Ways in which lists of releases can be ordered upon return. +input ReleaseOrder { + # The direction in which to order releases by the specified field. + direction: OrderDirection! + + # The field in which to order releases by. + field: ReleaseOrderField! +} + +# Properties by which release connections can be ordered. +enum ReleaseOrderField { + # Order releases by creation time + CREATED_AT + + # Order releases alphabetically by name + NAME +} + +# Autogenerated input type of RemoveOutsideCollaborator +input RemoveOutsideCollaboratorInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The ID of the organization to remove the outside collaborator from. + organizationId: ID! + + # The ID of the outside collaborator to remove. + userId: ID! +} + +# Autogenerated return type of RemoveOutsideCollaborator +type RemoveOutsideCollaboratorPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The user that was removed as an outside collaborator. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `removedUser` will change from `User!` to `User`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + removedUser: User! +} + +# Autogenerated input type of RemoveReaction +input RemoveReactionInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of the emoji reaction to remove. + content: ReactionContent! + + # The Node ID of the subject to modify. + subjectId: ID! +} + +# Autogenerated return type of RemoveReaction +type RemoveReactionPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The reaction object. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + reaction: Reaction! + + # The reactable subject. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + subject: Reactable! +} + +# Autogenerated input type of RemoveStar +input RemoveStarInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Starrable ID to unstar. + starrableId: ID! +} + +# Autogenerated return type of RemoveStar +type RemoveStarPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The starrable. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + starrable: Starrable! +} + +# Represents a 'removed_from_project' event on a given issue or pull request. +type RemovedFromProjectEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! +} + +# Represents a 'renamed' event on a given issue or pull request +type RenamedTitleEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the current title of the issue or pull request. + currentTitle: String! + id: ID! + + # Identifies the previous title of the issue or pull request. + previousTitle: String! + + # Subject that was renamed. + subject: RenamedTitleSubject! +} + +# An object which has a renamable title +union RenamedTitleSubject = Issue | PullRequest + +# Represents a 'reopened' event on any `Closable`. +type ReopenedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Object that was reopened. + closable: Closable! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! +} + +# A repository contains the content for a project. +type Repository implements Node & ProjectOwner & RegistryPackageOwner & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable { + # A list of users that can be assigned to issues in this repository. + assignableUsers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # Returns the code of conduct for this repository + codeOfConduct: CodeOfConduct + + # A list of collaborators associated with the repository. + collaborators( + # Collaborators affiliation level with a repository. + affiliation: CollaboratorAffiliation + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): RepositoryCollaboratorConnection + + # A list of commit comments associated with the repository. + commitComments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): CommitCommentConnection! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + + # The Ref associated with the repository's default branch. + defaultBranchRef: Ref + + # A list of deploy keys that are on this repository. + deployKeys( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): DeployKeyConnection! + + # Deployments associated with the repository + deployments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Environments to list deployments for + environments: [String!] + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): DeploymentConnection! + + # The description of the repository. + description: String + + # The description of the repository rendered to HTML. + descriptionHTML: HTML! + + # The number of kilobytes this repository occupies on disk. + diskUsage: Int + + # Returns how many forks there are of this repository in the whole network. + forkCount: Int! + + # A list of direct forked repositories. + forks( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # Indicates if the repository has issues feature enabled. + hasIssuesEnabled: Boolean! + + # Indicates if the repository has wiki feature enabled. + hasWikiEnabled: Boolean! + + # The repository's URL. + homepageUrl: URI + id: ID! + + # Indicates if the repository is unmaintained. + isArchived: Boolean! + + # Identifies if the repository is a fork. + isFork: Boolean! + + # Indicates if the repository has been locked or not. + isLocked: Boolean! + + # Identifies if the repository is a mirror. + isMirror: Boolean! + + # Identifies if the repository is private. + isPrivate: Boolean! + + # Returns a single issue from the current repository by number. + issue( + # The number for the issue to be returned. + number: Int! + ): Issue + + # Returns a single issue-like object from the current repository by number. + issueOrPullRequest( + # The number for the issue to be returned. + number: Int! + ): IssueOrPullRequest + + # A list of issues that have been opened in the repository. + issues( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for issues returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the issues by. + states: [IssueState!] + ): IssueConnection! + + # Returns a single label by name + label( + # Label name + name: String! + ): Label + + # A list of labels associated with the repository. + labels( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # If provided, searches labels by name and description. + query: String + ): LabelConnection + + # A list containing a breakdown of the language composition of the repository. + languages( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: LanguageOrder + ): LanguageConnection + + # The license associated with the repository + licenseInfo: License + + # The reason the repository has been locked. + lockReason: RepositoryLockReason + + # A list of Users that can be mentioned in the context of the repository. + mentionableUsers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! + + # Whether or not PRs are merged with a merge commit on this repository. + mergeCommitAllowed: Boolean! + + # Returns a single milestone from the current repository by number. + milestone( + # The number for the milestone to be returned. + number: Int! + ): Milestone + + # A list of milestones associated with the repository. + milestones( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for milestones. + orderBy: MilestoneOrder + + # Filter by the state of the milestones. + states: [MilestoneState!] + ): MilestoneConnection + + # The repository's original mirror URL. + mirrorUrl: URI + + # The name of the repository. + name: String! + + # The repository's name with owner. + nameWithOwner: String! + + # A Git object in the repository + object( + # A Git revision expression suitable for rev-parse + expression: String + + # The Git object ID + oid: GitObjectID + ): GitObject + + # The User owner of the repository. + owner: RepositoryOwner! + + # The repository parent, if this is a fork. + parent: Repository + + # The primary language of the repository's code. + primaryLanguage: Language + + # Find project by number. + project( + # The project number to find. + number: Int! + ): Project + + # A list of projects under the owner. + projects( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for projects returned from the connection + orderBy: ProjectOrder + + # Query to search projects by, currently only searching by name. + search: String + + # A list of states to filter the projects by. + states: [ProjectState!] + ): ProjectConnection! + + # The HTTP path listing the repository's projects + projectsResourcePath: URI! + + # The HTTP URL listing the repository's projects + projectsUrl: URI! + + # A list of protected branches that are on this repository. + protectedBranches( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): ProtectedBranchConnection! + + # Returns a single pull request from the current repository by number. + pullRequest( + # The number for the pull request to be returned. + number: Int! + ): PullRequest + + # A list of pull requests that have been opened in the repository. + pullRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # The base ref name to filter the pull requests by. + baseRefName: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # The head ref name to filter the pull requests by. + headRefName: String + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for pull requests returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the pull requests by. + states: [PullRequestState!] + ): PullRequestConnection! + + # Identifies when the repository was last pushed to. + pushedAt: DateTime + + # Whether or not rebase-merging is enabled on this repository. + rebaseMergeAllowed: Boolean! + + # Fetch a given ref from the repository + ref( + # The ref to retrieve. Fully qualified matches are checked in order + # (`refs/heads/master`) before falling back onto checks for short name matches (`master`). + qualifiedName: String! + ): Ref + + # Fetch a list of refs from the repository + refs( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # DEPRECATED: use orderBy. The ordering direction. + direction: OrderDirection + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for refs returned from the connection. + orderBy: RefOrder + + # A ref name prefix like `refs/heads/`, `refs/tags/`, etc. + refPrefix: String! + ): RefConnection + + # Lookup a single release given various criteria. + release( + # The name of the Tag the Release was created from + tagName: String! + ): Release + + # List of releases which are dependent on this repository. + releases( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: ReleaseOrder + ): ReleaseConnection! + + # A list of applied repository-topic associations for this repository. + repositoryTopics( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): RepositoryTopicConnection! + + # The HTTP path for this repository + resourcePath: URI! + + # A description of the repository, rendered to HTML without any links in it. + shortDescriptionHTML( + # How many characters to return. + limit: Int = 200 + ): HTML! + + # Whether or not squash-merging is enabled on this repository. + squashMergeAllowed: Boolean! + + # The SSH URL to clone this repository + sshUrl: GitSSHRemote! + + # A list of users who have starred this starrable. + stargazers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: StarOrder + ): StargazerConnection! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this repository + url: URI! + + # Indicates whether the viewer has admin permissions on this repository. + viewerCanAdminister: Boolean! + + # Can the current viewer create new projects on this owner. + viewerCanCreateProjects: Boolean! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Indicates whether the viewer can update the topics of this repository. + viewerCanUpdateTopics: Boolean! + + # Returns a boolean indicating whether the viewing user has starred this starrable. + viewerHasStarred: Boolean! + + # The users permission level on the repository. Will return null if authenticated as an GitHub App. + viewerPermission: RepositoryPermission + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState + + # A list of users watching the repository. + watchers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): UserConnection! +} + +# The affiliation of a user to a repository +enum RepositoryAffiliation { + # Repositories that the user has been added to as a collaborator. + COLLABORATOR + + # Repositories that the user has access to through being a member of an + # organization. This includes every repository on every team that the user is on. + ORGANIZATION_MEMBER + + # Repositories that are owned by the authenticated user. + OWNER +} + +# The affiliation type between collaborator and repository. +enum RepositoryCollaboratorAffiliation { + # All collaborators of the repository. + ALL + + # All outside collaborators of an organization-owned repository. + OUTSIDE +} + +# The connection type for User. +type RepositoryCollaboratorConnection { + # A list of edges. + edges: [RepositoryCollaboratorEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a user who is a collaborator of a repository. +type RepositoryCollaboratorEdge { + # A cursor for use in pagination. + cursor: String! + node: User! + + # The permission the user has on the repository. + permission: RepositoryPermission! +} + +# A list of repositories owned by the subject. +type RepositoryConnection { + # A list of edges. + edges: [RepositoryEdge] + + # A list of nodes. + nodes: [Repository] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! + + # The total size in kilobytes of all repositories in the connection. + totalDiskUsage: Int! +} + +# The reason a repository is listed as 'contributed'. +enum RepositoryContributionType { + # Created a commit + COMMIT + + # Created an issue + ISSUE + + # Created a pull request + PULL_REQUEST + + # Reviewed a pull request + PULL_REQUEST_REVIEW + + # Created the repository + REPOSITORY +} + +# An edge in a connection. +type RepositoryEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Repository +} + +# A subset of repository info. +interface RepositoryInfo { + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The description of the repository. + description: String + + # The description of the repository rendered to HTML. + descriptionHTML: HTML! + + # Returns how many forks there are of this repository in the whole network. + forkCount: Int! + + # Indicates if the repository has issues feature enabled. + hasIssuesEnabled: Boolean! + + # Indicates if the repository has wiki feature enabled. + hasWikiEnabled: Boolean! + + # The repository's URL. + homepageUrl: URI + + # Indicates if the repository is unmaintained. + isArchived: Boolean! + + # Identifies if the repository is a fork. + isFork: Boolean! + + # Indicates if the repository has been locked or not. + isLocked: Boolean! + + # Identifies if the repository is a mirror. + isMirror: Boolean! + + # Identifies if the repository is private. + isPrivate: Boolean! + + # The license associated with the repository + licenseInfo: License + + # The reason the repository has been locked. + lockReason: RepositoryLockReason + + # The repository's original mirror URL. + mirrorUrl: URI + + # The name of the repository. + name: String! + + # The repository's name with owner. + nameWithOwner: String! + + # The User owner of the repository. + owner: RepositoryOwner! + + # Identifies when the repository was last pushed to. + pushedAt: DateTime + + # The HTTP path for this repository + resourcePath: URI! + + # A description of the repository, rendered to HTML without any links in it. + shortDescriptionHTML( + # How many characters to return. + limit: Int = 200 + ): HTML! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this repository + url: URI! +} + +# An invitation for a user to be added to a repository. +type RepositoryInvitation implements Node { + id: ID! + + # The user who received the invitation. + invitee: User! + + # The user who created the invitation. + inviter: User! + + # The permission granted on this repository by this invitation. + permission: RepositoryPermission! + + # The Repository the user is invited to. + repository: RepositoryInfo +} + +# An edge in a connection. +type RepositoryInvitationEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: RepositoryInvitation +} + +# The possible reasons a given repository could be in a locked state. +enum RepositoryLockReason { + # The repository is locked due to a billing related reason. + BILLING + + # The repository is locked due to a migration. + MIGRATING + + # The repository is locked due to a move. + MOVING + + # The repository is locked due to a rename. + RENAME +} + +# Represents a object that belongs to a repository. +interface RepositoryNode { + # The repository associated with this node. + repository: Repository! +} + +# Ordering options for repository connections +input RepositoryOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order repositories by. + field: RepositoryOrderField! +} + +# Properties by which repository connections can be ordered. +enum RepositoryOrderField { + # Order repositories by creation time + CREATED_AT + + # Order repositories by name + NAME + + # Order repositories by push time + PUSHED_AT + + # Order repositories by number of stargazers + STARGAZERS + + # Order repositories by update time + UPDATED_AT +} + +# Represents an owner of a Repository. +interface RepositoryOwner { + # A URL pointing to the owner's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + id: ID! + + # The username used to login. + login: String! + + # A list of repositories this user has pinned to their profile + pinnedRepositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # A list of repositories that the user owns. + repositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they are forks of another repository + isFork: Boolean + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # Find Repository. + repository( + # Name of Repository to find. + name: String! + ): Repository + + # The HTTP URL for the owner. + resourcePath: URI! + + # The HTTP URL for the owner. + url: URI! +} + +# The access level to a repository +enum RepositoryPermission { + # Can read, clone, push, and add collaborators + ADMIN + + # Can read and clone + READ + + # Can read, clone and push + WRITE +} + +# The privacy of a repository +enum RepositoryPrivacy { + # Private + PRIVATE + + # Public + PUBLIC +} + +# A repository-topic connects a repository to a topic. +type RepositoryTopic implements Node & UniformResourceLocatable { + id: ID! + + # The HTTP path for this repository-topic. + resourcePath: URI! + + # The topic. + topic: Topic! + + # The HTTP URL for this repository-topic. + url: URI! +} + +# The connection type for RepositoryTopic. +type RepositoryTopicConnection { + # A list of edges. + edges: [RepositoryTopicEdge] + + # A list of nodes. + nodes: [RepositoryTopic] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type RepositoryTopicEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: RepositoryTopic +} + +# Autogenerated input type of RequestReviews +input RequestReviewsInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the pull request to modify. + pullRequestId: ID! + + # The Node IDs of the team to request. + teamIds: [ID!] + + # Add users to the set rather than replace. + union: Boolean + + # The Node IDs of the user to request. + userIds: [ID!] +} + +# Autogenerated return type of RequestReviews +type RequestReviewsPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The pull request that is getting requests. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequest` will change from `PullRequest!` to `PullRequest`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequest: PullRequest! + + # The edge from the pull request to the requested reviewers. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `requestedReviewersEdge` will change from `UserEdge!` to `UserEdge`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + requestedReviewersEdge: UserEdge! +} + +# Types that can be requested reviewers. +union RequestedReviewer = Team | User + +# A team or user who has the ability to dismiss a review on a protected branch. +type ReviewDismissalAllowance implements Node { + # The actor that can dismiss. + actor: ReviewDismissalAllowanceActor + id: ID! + + # Identifies the protected branch associated with the allowed user or team. + protectedBranch: ProtectedBranch! +} + +# Types that can be an actor. +union ReviewDismissalAllowanceActor = Team | User + +# The connection type for ReviewDismissalAllowance. +type ReviewDismissalAllowanceConnection { + # A list of edges. + edges: [ReviewDismissalAllowanceEdge] + + # A list of nodes. + nodes: [ReviewDismissalAllowance] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ReviewDismissalAllowanceEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ReviewDismissalAllowance +} + +# Represents a 'review_dismissed' event on a given issue or pull request. +type ReviewDismissedEvent implements Node & UniformResourceLocatable { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # Identifies the message associated with the 'review_dismissed' event. + message: String! + + # The message associated with the event, rendered to HTML. + messageHtml: HTML! + + # Identifies the previous state of the review with the 'review_dismissed' event. + previousReviewState: PullRequestReviewState! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # Identifies the commit which caused the review to become stale. + pullRequestCommit: PullRequestCommit + + # The HTTP path for this review dismissed event. + resourcePath: URI! + + # Identifies the review associated with the 'review_dismissed' event. + review: PullRequestReview + + # The HTTP URL for this review dismissed event. + url: URI! +} + +# A request for a user to review a pull request. +type ReviewRequest implements Node { + # Identifies the primary key from the database. + databaseId: Int + id: ID! + + # Identifies the pull request associated with this review request. + pullRequest: PullRequest! + + # The reviewer that is requested. + requestedReviewer: RequestedReviewer +} + +# The connection type for ReviewRequest. +type ReviewRequestConnection { + # A list of edges. + edges: [ReviewRequestEdge] + + # A list of nodes. + nodes: [ReviewRequest] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type ReviewRequestEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: ReviewRequest +} + +# Represents an 'review_request_removed' event on a given pull request. +type ReviewRequestRemovedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # Identifies the reviewer whose review request was removed. + requestedReviewer: RequestedReviewer +} + +# Represents an 'review_requested' event on a given pull request. +type ReviewRequestedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # PullRequest referenced by event. + pullRequest: PullRequest! + + # Identifies the reviewer whose review was requested. + requestedReviewer: RequestedReviewer +} + +# The results of a search. +union SearchResultItem = Issue | MarketplaceListing | Organization | PullRequest | Repository | User + +# A list of results that matched against a search query. +type SearchResultItemConnection { + # The number of pieces of code that matched the search query. + codeCount: Int! + + # A list of edges. + edges: [SearchResultItemEdge] + + # The number of issues that matched the search query. + issueCount: Int! + + # A list of nodes. + nodes: [SearchResultItem] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # The number of repositories that matched the search query. + repositoryCount: Int! + + # The number of users that matched the search query. + userCount: Int! + + # The number of wiki pages that matched the search query. + wikiCount: Int! +} + +# An edge in a connection. +type SearchResultItemEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: SearchResultItem + + # Text matches on the result found. + textMatches: [TextMatch] +} + +# Represents the individual results of a search. +enum SearchType { + # Returns results matching issues in repositories. + ISSUE + + # Returns results matching repositories. + REPOSITORY + + # Returns results matching users and organizations on GitHub. + USER +} + +# Represents an S/MIME signature on a Commit or Tag. +type SmimeSignature implements GitSignature { + # Email used to sign this object. + email: String! + + # True if the signature is valid and verified by GitHub. + isValid: Boolean! + + # Payload for GPG signing object. Raw ODB object without the signature header. + payload: String! + + # ASCII-armored signature header from object. + signature: String! + + # GitHub user corresponding to the email signing this commit. + signer: User + + # The state of this signature. `VALID` if signature is valid and verified by + # GitHub, otherwise represents reason why signature is considered invalid. + state: GitSignatureState! + + # True if the signature was made with GitHub's signing key. + wasSignedByGitHub: Boolean! +} + +# Ways in which star connections can be ordered. +input StarOrder { + # The direction in which to order nodes. + direction: OrderDirection! + + # The field in which to order nodes by. + field: StarOrderField! +} + +# Properties by which star connections can be ordered. +enum StarOrderField { + # Allows ordering a list of stars by when they were created. + STARRED_AT +} + +# The connection type for User. +type StargazerConnection { + # A list of edges. + edges: [StargazerEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a user that's starred a repository. +type StargazerEdge { + # A cursor for use in pagination. + cursor: String! + node: User! + + # Identifies when the item was starred. + starredAt: DateTime! +} + +# Things that can be starred. +interface Starrable { + id: ID! + + # A list of users who have starred this starrable. + stargazers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: StarOrder + ): StargazerConnection! + + # Returns a boolean indicating whether the viewing user has starred this starrable. + viewerHasStarred: Boolean! +} + +# The connection type for Repository. +type StarredRepositoryConnection { + # A list of edges. + edges: [StarredRepositoryEdge] + + # A list of nodes. + nodes: [Repository] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a starred repository. +type StarredRepositoryEdge { + # A cursor for use in pagination. + cursor: String! + node: Repository! + + # Identifies when the item was starred. + starredAt: DateTime! +} + +# Represents a commit status. +type Status implements Node { + # The commit this status is attached to. + commit: Commit + + # Looks up an individual status context by context name. + context( + # The context name. + name: String! + ): StatusContext + + # The individual status contexts for this commit. + contexts: [StatusContext!]! + id: ID! + + # The combined commit status. + state: StatusState! +} + +# Represents an individual commit status context +type StatusContext implements Node { + # This commit this status context is attached to. + commit: Commit + + # The name of this status context. + context: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The actor who created this status context. + creator: Actor + + # The description for this status context. + description: String + id: ID! + + # The state of this status context. + state: StatusState! + + # The URL for this status context. + targetUrl: URI +} + +# The possible commit status states. +enum StatusState { + # Status is errored. + ERROR + + # Status is expected. + EXPECTED + + # Status is failing. + FAILURE + + # Status is pending. + PENDING + + # Status is successful. + SUCCESS +} + +# Autogenerated input type of SubmitPullRequestReview +input SubmitPullRequestReviewInput { + # The text field to set on the Pull Request Review. + body: String + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The event to send to the Pull Request Review. + event: PullRequestReviewEvent! + + # The Pull Request Review ID to submit. + pullRequestReviewId: ID! +} + +# Autogenerated return type of SubmitPullRequestReview +type SubmitPullRequestReviewPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The submitted pull request review. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReview: PullRequestReview! +} + +# Entities that can be subscribed to for web and email notifications. +interface Subscribable { + id: ID! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState +} + +# Represents a 'subscribed' event on a given `Subscribable`. +type SubscribedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Object referenced by event. + subscribable: Subscribable! +} + +# The possible states of a subscription. +enum SubscriptionState { + # The User is never notified. + IGNORED + + # The User is notified of all conversations. + SUBSCRIBED + + # The User is only notified when particpating or @mentioned. + UNSUBSCRIBED +} + +# A suggestion to review a pull request based on a user's commit history and review comments. +type SuggestedReviewer { + # Is this suggestion based on past commits? + isAuthor: Boolean! + + # Is this suggestion based on past review comments? + isCommenter: Boolean! + + # Identifies the user suggested to review the pull request. + reviewer: User! +} + +# Represents a Git tag. +type Tag implements GitObject & Node { + # An abbreviated version of the Git object ID + abbreviatedOid: String! + + # The HTTP path for this Git object + commitResourcePath: URI! + + # The HTTP URL for this Git object + commitUrl: URI! + id: ID! + + # The Git tag message. + message: String + + # The Git tag name. + name: String! + + # The Git object ID + oid: GitObjectID! + + # The Repository the Git object belongs to + repository: Repository! + + # Details about the tag author. + tagger: GitActor + + # The Git object the tag points to. + target: GitObject! +} + +# A team of users in an organization. +type Team implements Node & Subscribable { + # A list of teams that are ancestors of this team. + ancestors( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): TeamConnection! + + # A URL pointing to the team's avatar. + avatarUrl( + # The size in pixels of the resulting square image. + size: Int = 400 + ): URI + + # List of child teams belonging to this team + childTeams( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Whether to list immediate child teams or all descendant child teams. + immediateOnly: Boolean = true + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: TeamOrder + + # User logins to filter by + userLogins: [String!] + ): TeamConnection! + + # The slug corresponding to the organization and team. + combinedSlug: String! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # The description of the team. + description: String + + # The HTTP path for editing this team + editTeamResourcePath: URI! + + # The HTTP URL for editing this team + editTeamUrl: URI! + id: ID! + + # A list of pending invitations for users to this team + invitations( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): OrganizationInvitationConnection + + # A list of users who are members of this team. + members( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Filter by membership type + membership: TeamMembershipType = ALL + + # Order for the connection. + orderBy: TeamMemberOrder + + # The search string to look for. + query: String + + # Filter by team member role + role: TeamMemberRole + ): TeamMemberConnection! + + # The HTTP path for the team' members + membersResourcePath: URI! + + # The HTTP URL for the team' members + membersUrl: URI! + + # The name of the team. + name: String! + + # The HTTP path creating a new team + newTeamResourcePath: URI! + + # The HTTP URL creating a new team + newTeamUrl: URI! + + # The organization that owns this team. + organization: Organization! + + # The parent team of the team. + parentTeam: Team + + # The level of privacy the team has. + privacy: TeamPrivacy! + + # A list of repositories this team has access to. + repositories( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for the connection. + orderBy: TeamRepositoryOrder + + # The search string to look for. + query: String + ): TeamRepositoryConnection! + + # The HTTP path for this team's repositories + repositoriesResourcePath: URI! + + # The HTTP URL for this team's repositories + repositoriesUrl: URI! + + # The HTTP path for this team + resourcePath: URI! + + # The slug corresponding to the team. + slug: String! + + # The HTTP path for this team's teams + teamsResourcePath: URI! + + # The HTTP URL for this team's teams + teamsUrl: URI! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this team + url: URI! + + # Team is adminable by the viewer. + viewerCanAdminister: Boolean! + + # Check if the viewer is able to change their subscription status for the repository. + viewerCanSubscribe: Boolean! + + # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. + viewerSubscription: SubscriptionState +} + +# The connection type for Team. +type TeamConnection { + # A list of edges. + edges: [TeamEdge] + + # A list of nodes. + nodes: [Team] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type TeamEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Team +} + +# The connection type for User. +type TeamMemberConnection { + # A list of edges. + edges: [TeamMemberEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a user who is a member of a team. +type TeamMemberEdge { + # A cursor for use in pagination. + cursor: String! + + # The HTTP path to the organization's member access page. + memberAccessResourcePath: URI! + + # The HTTP URL to the organization's member access page. + memberAccessUrl: URI! + node: User! + + # The role the member has on the team. + role: TeamMemberRole! +} + +# Ordering options for team member connections +input TeamMemberOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order team members by. + field: TeamMemberOrderField! +} + +# Properties by which team member connections can be ordered. +enum TeamMemberOrderField { + # Order team members by creation time + CREATED_AT + + # Order team members by login + LOGIN +} + +# The possible team member roles; either 'maintainer' or 'member'. +enum TeamMemberRole { + # A team maintainer has permission to add and remove team members. + MAINTAINER + + # A team member has no administrative permissions on the team. + MEMBER +} + +# Defines which types of team members are included in the returned list. Can be one of IMMEDIATE, CHILD_TEAM or ALL. +enum TeamMembershipType { + # Includes immediate and child team members for the team. + ALL + + # Includes only child team members for the team. + CHILD_TEAM + + # Includes only immediate members of the team. + IMMEDIATE +} + +# Ways in which team connections can be ordered. +input TeamOrder { + # The direction in which to order nodes. + direction: OrderDirection! + + # The field in which to order nodes by. + field: TeamOrderField! +} + +# Properties by which team connections can be ordered. +enum TeamOrderField { + # Allows ordering a list of teams by name. + NAME +} + +# The possible team privacy values. +enum TeamPrivacy { + # A secret team can only be seen by its members. + SECRET + + # A visible team can be seen and @mentioned by every member of the organization. + VISIBLE +} + +# The connection type for Repository. +type TeamRepositoryConnection { + # A list of edges. + edges: [TeamRepositoryEdge] + + # A list of nodes. + nodes: [Repository] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# Represents a team repository. +type TeamRepositoryEdge { + # A cursor for use in pagination. + cursor: String! + node: Repository! + + # The permission level the team has on the repository + permission: RepositoryPermission! +} + +# Ordering options for team repository connections +input TeamRepositoryOrder { + # The ordering direction. + direction: OrderDirection! + + # The field to order repositories by. + field: TeamRepositoryOrderField! +} + +# Properties by which team repository connections can be ordered. +enum TeamRepositoryOrderField { + # Order repositories by creation time + CREATED_AT + + # Order repositories by name + NAME + + # Order repositories by permission + PERMISSION + + # Order repositories by push time + PUSHED_AT + + # Order repositories by number of stargazers + STARGAZERS + + # Order repositories by update time + UPDATED_AT +} + +# The role of a user on a team. +enum TeamRole { + # User has admin rights on the team. + ADMIN + + # User is a member of the team. + MEMBER +} + +# A text match within a search result. +type TextMatch { + # The specific text fragment within the property matched on. + fragment: String! + + # Highlights within the matched fragment. + highlights: [TextMatchHighlight!]! + + # The property matched on. + property: String! +} + +# Represents a single highlight in a search result match. +type TextMatchHighlight { + # The indice in the fragment where the matched text begins. + beginIndice: Int! + + # The indice in the fragment where the matched text ends. + endIndice: Int! + + # The text matched. + text: String! +} + +# A topic aggregates entities that are related to a subject. +type Topic implements Node { + id: ID! + + # The topic's name. + name: String! + + # A list of related topics, including aliases of this topic, sorted with the most relevant + # first. + relatedTopics: [Topic!]! +} + +# The connection type for Topic. +type TopicConnection { + # A list of edges. + edges: [TopicEdge] + + # A list of nodes. + nodes: [Topic] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type TopicEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: Topic +} + +# Reason that the suggested topic is declined. +enum TopicSuggestionDeclineReason { + # The suggested topic is not relevant to the repository. + NOT_RELEVANT + + # The viewer does not like the suggested topic. + PERSONAL_PREFERENCE + + # The suggested topic is too general for the repository. + TOO_GENERAL + + # The suggested topic is too specific for the repository (e.g. #ruby-on-rails-version-4-2-1). + TOO_SPECIFIC +} + +# Represents a Git tree. +type Tree implements GitObject & Node { + # An abbreviated version of the Git object ID + abbreviatedOid: String! + + # The HTTP path for this Git object + commitResourcePath: URI! + + # The HTTP URL for this Git object + commitUrl: URI! + + # A list of tree entries. + entries: [TreeEntry!] + id: ID! + + # The Git object ID + oid: GitObjectID! + + # The Repository the Git object belongs to + repository: Repository! +} + +# Represents a Git tree entry. +type TreeEntry { + # Entry file mode. + mode: Int! + + # Entry file name. + name: String! + + # Entry file object. + object: GitObject + + # Entry file Git object ID. + oid: GitObjectID! + + # The Repository the tree entry belongs to + repository: Repository! + + # Entry file type. + type: String! +} + +# An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. +scalar URI + +# Represents an 'unassigned' event on any assignable object. +type UnassignedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the assignable associated with the event. + assignable: Assignable! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the subject (user) who was unassigned. + user: User +} + +# Represents a type that can be retrieved by a URL. +interface UniformResourceLocatable { + # The HTML path to this resource. + resourcePath: URI! + + # The URL to this resource. + url: URI! +} + +# Represents an unknown signature on a Commit or Tag. +type UnknownSignature implements GitSignature { + # Email used to sign this object. + email: String! + + # True if the signature is valid and verified by GitHub. + isValid: Boolean! + + # Payload for GPG signing object. Raw ODB object without the signature header. + payload: String! + + # ASCII-armored signature header from object. + signature: String! + + # GitHub user corresponding to the email signing this commit. + signer: User + + # The state of this signature. `VALID` if signature is valid and verified by + # GitHub, otherwise represents reason why signature is considered invalid. + state: GitSignatureState! + + # True if the signature was made with GitHub's signing key. + wasSignedByGitHub: Boolean! +} + +# Represents an 'unlabeled' event on a given issue or pull request. +type UnlabeledEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Identifies the label associated with the 'unlabeled' event. + label: Label! + + # Identifies the `Labelable` associated with the event. + labelable: Labelable! +} + +# Autogenerated input type of UnlockLockable +input UnlockLockableInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # ID of the issue or pull request to be unlocked. + lockableId: ID! +} + +# Autogenerated return type of UnlockLockable +type UnlockLockablePayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The item that was unlocked. + unlockedRecord: Lockable +} + +# Represents an 'unlocked' event on a given issue or pull request. +type UnlockedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Object that was unlocked. + lockable: Lockable! +} + +# Represents an 'unsubscribed' event on a given `Subscribable`. +type UnsubscribedEvent implements Node { + # Identifies the actor who performed the event. + actor: Actor + + # Identifies the date and time when the object was created. + createdAt: DateTime! + id: ID! + + # Object referenced by event. + subscribable: Subscribable! +} + +# Entities that can be updated. +interface Updatable { + # Check if the current viewer can update this object. + viewerCanUpdate: Boolean! +} + +# Comments that can be updated. +interface UpdatableComment { + # Reasons why the current viewer can not update this comment. + viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! +} + +# Autogenerated input type of UpdateProjectCard +input UpdateProjectCardInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # Whether or not the ProjectCard should be archived + isArchived: Boolean + + # The note of ProjectCard. + note: String + + # The ProjectCard ID to update. + projectCardId: ID! +} + +# Autogenerated return type of UpdateProjectCard +type UpdateProjectCardPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The updated ProjectCard. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `projectCard` will change from `ProjectCard!` to `ProjectCard`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + projectCard: ProjectCard! +} + +# Autogenerated input type of UpdateProjectColumn +input UpdateProjectColumnInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of project column. + name: String! + + # The ProjectColumn ID to update. + projectColumnId: ID! +} + +# Autogenerated return type of UpdateProjectColumn +type UpdateProjectColumnPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The updated project column. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `projectColumn` will change from `ProjectColumn!` to `ProjectColumn`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + projectColumn: ProjectColumn! +} + +# Autogenerated input type of UpdateProject +input UpdateProjectInput { + # The description of project. + body: String + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The name of project. + name: String + + # The Project ID to update. + projectId: ID! + + # Whether the project is public or not. + public: Boolean + + # Whether the project is open or closed. + state: ProjectState +} + +# Autogenerated return type of UpdateProject +type UpdateProjectPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The updated project. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `project` will change from `Project!` to `Project`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + project: Project! +} + +# Autogenerated input type of UpdatePullRequestReviewComment +input UpdatePullRequestReviewCommentInput { + # The text of the comment. + body: String! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the comment to modify. + pullRequestReviewCommentId: ID! +} + +# Autogenerated return type of UpdatePullRequestReviewComment +type UpdatePullRequestReviewCommentPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The updated comment. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReviewComment` will change from + # `PullRequestReviewComment!` to `PullRequestReviewComment`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReviewComment: PullRequestReviewComment! +} + +# Autogenerated input type of UpdatePullRequestReview +input UpdatePullRequestReviewInput { + # The contents of the pull request review body. + body: String! + + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the pull request review to modify. + pullRequestReviewId: ID! +} + +# Autogenerated return type of UpdatePullRequestReview +type UpdatePullRequestReviewPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The updated pull request review. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + pullRequestReview: PullRequestReview! +} + +# Autogenerated input type of UpdateSubscription +input UpdateSubscriptionInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The new state of the subscription. + state: SubscriptionState! + + # The Node ID of the subscribable object to modify. + subscribableId: ID! +} + +# Autogenerated return type of UpdateSubscription +type UpdateSubscriptionPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The input subscribable entity. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `subscribable` will change from `Subscribable!` to `Subscribable`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + subscribable: Subscribable! +} + +# Autogenerated input type of UpdateTopics +input UpdateTopicsInput { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # The Node ID of the repository. + repositoryId: ID! + + # An array of topic names. + topicNames: [String!]! +} + +# Autogenerated return type of UpdateTopics +type UpdateTopicsPayload { + # A unique identifier for the client performing the mutation. + clientMutationId: String + + # Names of the provided topics that are not valid. + invalidTopicNames: [String!] + + # The updated repository. + # + # **Upcoming Change on 2019-01-01 UTC** + # **Description:** Type for `repository` will change from `Repository!` to `Repository`. + # **Reason:** In preparation for an upcoming change to the way we report + # mutation errors, non-nullable payload fields are becoming nullable. + repository: Repository! +} + +# A user is an individual's account on GitHub that owns repositories and can make new content. +type User implements Actor & Node & RegistryPackageOwner & RegistryPackageSearch & RepositoryOwner & UniformResourceLocatable { + # A URL pointing to the user's public avatar. + avatarUrl( + # The size of the resulting square image. + size: Int + ): URI! + + # The user's public profile bio. + bio: String + + # The user's public profile bio as HTML. + bioHTML: HTML! + + # A list of commit comments made by this user. + commitComments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): CommitCommentConnection! + + # The user's public profile company. + company: String + + # The user's public profile company as HTML. + companyHTML: HTML! + + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the primary key from the database. + databaseId: Int + + # The user's publicly visible profile email. + email: String! + + # A list of users the given user is followed by. + followers( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): FollowerConnection! + + # A list of users the given user is following. + following( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): FollowingConnection! + + # Find gist by repo name. + gist( + # The gist name to find. + name: String! + ): Gist + + # A list of gist comments made by this user. + gistComments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): GistCommentConnection! + + # A list of the Gists the user has created. + gists( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for gists returned from the connection + orderBy: GistOrder + + # Filters Gists according to privacy. + privacy: GistPrivacy + ): GistConnection! + id: ID! + + # Whether or not this user is a participant in the GitHub Security Bug Bounty. + isBountyHunter: Boolean! + + # Whether or not this user is a participant in the GitHub Campus Experts Program. + isCampusExpert: Boolean! + + # Whether or not this user is a GitHub Developer Program member. + isDeveloperProgramMember: Boolean! + + # Whether or not this user is a GitHub employee. + isEmployee: Boolean! + + # Whether or not the user has marked themselves as for hire. + isHireable: Boolean! + + # Whether or not this user is a site administrator. + isSiteAdmin: Boolean! + + # Whether or not this user is the viewing user. + isViewer: Boolean! + + # A list of issue comments made by this user. + issueComments( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): IssueCommentConnection! + + # A list of issues associated with this user. + issues( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for issues returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the issues by. + states: [IssueState!] + ): IssueConnection! + + # The user's public profile location. + location: String + + # The username used to login. + login: String! + + # The user's public profile name. + name: String + + # Find an organization by its login that the user belongs to. + organization( + # The login of the organization to find. + login: String! + ): Organization + + # A list of organizations the user belongs to. + organizations( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): OrganizationConnection! + + # A list of repositories this user has pinned to their profile + pinnedRepositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # A list of public keys associated with this user. + publicKeys( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + ): PublicKeyConnection! + + # A list of pull requests associated with this user. + pullRequests( + # Returns the elements in the list that come after the specified cursor. + after: String + + # The base ref name to filter the pull requests by. + baseRefName: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # The head ref name to filter the pull requests by. + headRefName: String + + # A list of label names to filter the pull requests by. + labels: [String!] + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for pull requests returned from the connection. + orderBy: IssueOrder + + # A list of states to filter the pull requests by. + states: [PullRequestState!] + ): PullRequestConnection! + + # A list of repositories that the user owns. + repositories( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they are forks of another repository + isFork: Boolean + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # A list of repositories that the user recently contributed to. + repositoriesContributedTo( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # If non-null, include only the specified types of contributions. The + # GitHub.com UI uses [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] + contributionTypes: [RepositoryContributionType] + + # Returns the first _n_ elements from the list. + first: Int + + # If true, include user repositories + includeUserRepositories: Boolean + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # Find Repository. + repository( + # Name of Repository to find. + name: String! + ): Repository + + # The HTTP path for this user + resourcePath: URI! + + # Repositories the user has starred. + starredRepositories( + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # Returns the last _n_ elements from the list. + last: Int + + # Order for connection + orderBy: StarOrder + + # Filters starred repositories to only return repositories owned by the viewer. + ownedByViewer: Boolean + ): StarredRepositoryConnection! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! + + # The HTTP URL for this user + url: URI! + + # Whether or not the viewer is able to follow the user. + viewerCanFollow: Boolean! + + # Whether or not this user is followed by the viewer. + viewerIsFollowing: Boolean! + + # A list of repositories the given user is watching. + watching( + # Affiliation options for repositories returned from the connection + affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR, ORGANIZATION_MEMBER] + + # Returns the elements in the list that come after the specified cursor. + after: String + + # Returns the elements in the list that come before the specified cursor. + before: String + + # Returns the first _n_ elements from the list. + first: Int + + # If non-null, filters repositories according to whether they have been locked + isLocked: Boolean + + # Returns the last _n_ elements from the list. + last: Int + + # Ordering options for repositories returned from the connection + orderBy: RepositoryOrder + + # If non-null, filters repositories according to privacy + privacy: RepositoryPrivacy + ): RepositoryConnection! + + # A URL pointing to the user's public website/blog. + websiteUrl: URI +} + +# The connection type for User. +type UserConnection { + # A list of edges. + edges: [UserEdge] + + # A list of nodes. + nodes: [User] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edit on user content +type UserContentEdit implements Node { + # Identifies the date and time when the object was created. + createdAt: DateTime! + + # Identifies the date and time when the object was deleted. + deletedAt: DateTime + + # The actor who deleted this content + deletedBy: Actor + + # A summary of the changes for this edit + diff: String + + # When this content was edited + editedAt: DateTime! + + # The actor who edited this content + editor: Actor + id: ID! + + # Identifies the date and time when the object was last updated. + updatedAt: DateTime! +} + +# A list of edits to content. +type UserContentEditConnection { + # A list of edges. + edges: [UserContentEditEdge] + + # A list of nodes. + nodes: [UserContentEdit] + + # Information to aid in pagination. + pageInfo: PageInfo! + + # Identifies the total count of items in the connection. + totalCount: Int! +} + +# An edge in a connection. +type UserContentEditEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: UserContentEdit +} + +# Represents a user. +type UserEdge { + # A cursor for use in pagination. + cursor: String! + + # The item at the end of the edge. + node: User +} + +# A valid x509 certificate string +scalar X509Certificate diff --git a/src/ghstack/ghstack/github_utils.py b/src/ghstack/ghstack/github_utils.py new file mode 100644 index 0000000..ecb4b70 --- /dev/null +++ b/src/ghstack/ghstack/github_utils.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +import re +from typing import Optional + +from typing_extensions import TypedDict + +import ghstack.github +import ghstack.shell +from ghstack.types import GitHubRepositoryId + +GitHubRepoNameWithOwner = TypedDict( + "GitHubRepoNameWithOwner", + { + "owner": str, + "name": str, + }, +) + + +def get_github_repo_name_with_owner( + *, + sh: ghstack.shell.Shell, + github_url: str, + remote_name: str, +) -> GitHubRepoNameWithOwner: + # Grovel in remotes to figure it out + remote_url = sh.git("remote", "get-url", remote_name) + while True: + match = r"^git@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format( + github_url=github_url + ) + m = re.match(match, remote_url) + if m: + owner = m.group(1) + name = m.group(2) + break + search = r"{github_url}/([^/]+)/(.+?)(?:\.git)?$".format(github_url=github_url) + m = re.search(search, remote_url) + if m: + owner = m.group(1) + name = m.group(2) + break + raise RuntimeError( + "Couldn't determine repo owner and name from url: {}".format(remote_url) + ) + return {"owner": owner, "name": name} + + +GitHubRepoInfo = TypedDict( + "GitHubRepoInfo", + { + "name_with_owner": GitHubRepoNameWithOwner, + "id": GitHubRepositoryId, + "is_fork": bool, + "default_branch": str, + }, +) + + +def get_github_repo_info( + *, + github: ghstack.github.GitHubEndpoint, + sh: ghstack.shell.Shell, + repo_owner: Optional[str] = None, + repo_name: Optional[str] = None, + github_url: str, + remote_name: str, +) -> GitHubRepoInfo: + if repo_owner is None or repo_name is None: + name_with_owner = get_github_repo_name_with_owner( + sh=sh, + github_url=github_url, + remote_name=remote_name, + ) + else: + name_with_owner = {"owner": repo_owner, "name": repo_name} + + # TODO: Cache this guy + repo = github.graphql( + """ + query ($owner: String!, $name: String!) { + repository(name: $name, owner: $owner) { + id + isFork + defaultBranchRef { + name + } + } + }""", + owner=name_with_owner["owner"], + name=name_with_owner["name"], + )["data"]["repository"] + + return { + "name_with_owner": name_with_owner, + "id": repo["id"], + "is_fork": repo["isFork"], + "default_branch": repo["defaultBranchRef"]["name"], + } + + +RE_PR_URL = re.compile( + r"^https://(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/pull/(?P[0-9]+)/?$" +) + +GitHubPullRequestParams = TypedDict( + "GitHubPullRequestParams", + { + "github_url": str, + "owner": str, + "name": str, + "number": int, + }, +) + + +def parse_pull_request( + pull_request: str, + *, + sh: Optional[ghstack.shell.Shell] = None, + remote_name: Optional[str] = None, +) -> GitHubPullRequestParams: + m = RE_PR_URL.match(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", remote_name) + # Do not pass the shell to avoid infinite loop + try: + return parse_pull_request(remote_url + "/pull/" + pull_request) + except RuntimeError: + # Fall back on original error message + pass + raise RuntimeError("Did not understand PR argument. PR must be URL") + + github_url = m.group("github_url") + owner = m.group("owner") + name = m.group("name") + number = int(m.group("number")) + return {"github_url": github_url, "owner": owner, "name": name, "number": number} diff --git a/src/ghstack/ghstack/gpg_sign.py b/src/ghstack/ghstack/gpg_sign.py new file mode 100644 index 0000000..f4c01f2 --- /dev/null +++ b/src/ghstack/ghstack/gpg_sign.py @@ -0,0 +1,41 @@ +"""This module determines if the commits need to be signed. +We need to do this manually, because ghstack uses commit-tree instead of commit. +commit-tree command doesn't pick up commit.gpgsign git config + +The porcelain git behavior w.r.t. signing is + +when both `commit.gpgsign` and `user.signingkey` are set, the commit is signed +when only `commit.gpgsign` is true, git errors out + +This module will retain this behavior: +We will attempt to sign as long as `commit.gpgsign` is true. +If not key is configure, error will occur +""" + +from typing import Tuple, Union + +import ghstack.shell + +_should_sign = None + + +def gpg_args_if_necessary( + shell: ghstack.shell.Shell = ghstack.shell.Shell(), +) -> Union[Tuple[str], Tuple[()]]: + global _should_sign + # cache the config result + if _should_sign is None: + # If the config is not set, we get exit 1 + 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 ( + "yes", + "on", + "true", + "1", + ) + except: + _should_sign = False + + return ("-S",) if _should_sign else () diff --git a/src/ghstack/ghstack/land.py b/src/ghstack/ghstack/land.py new file mode 100644 index 0000000..5e411fe --- /dev/null +++ b/src/ghstack/ghstack/land.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 + +import logging +import re +from typing import List, Optional, Tuple + +import ghstack.git +import ghstack.github +import ghstack.github_utils +import ghstack.shell +from ghstack.diff import PullRequestResolved +from ghstack.types import GitCommitHash + + +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( + """ + query ($owner: String!, $name: String!, $number: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $number) { + headRefName + closed + } + } + } + """, + owner=owner, + name=name, + number=number, + ) + pr = pr_result["data"]["repository"]["pullRequest"] + head_ref = pr["headRefName"] + closed = pr["closed"] + assert isinstance(head_ref, str) + orig_ref = re.sub(r"/head$", "/orig", head_ref) + if orig_ref == head_ref: + raise RuntimeError( + "The ref {} doesn't look like a ghstack reference".format(head_ref) + ) + return orig_ref, closed + + +def main( + pull_request: str, + remote_name: str, + github: ghstack.github.GitHubEndpoint, + sh: ghstack.shell.Shell, + github_url: str, + *, + force: bool = False, + repo_default_branch_opt: Optional[str] = None, +) -> None: + + # We land the entire stack pointed to by a URL. + # Local state is ignored; PR is source of truth + # 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( + pull_request, sh=sh, remote_name=remote_name + ) + default_branch = repo_default_branch_opt + if default_branch is None: + default_branch = 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"] + + needs_force = False + try: + protection = github.get( + f"repos/{params['owner']}/{params['name']}/branches/{default_branch}/protection" + ) + if not protection["allow_force_pushes"]["enabled"]: + raise RuntimeError( + """\ +Default branch {default_branch} is protected, and doesn't allow force pushes. +ghstack land does not work. You will not be able to land your ghstack; please +resubmit your PRs using the normal pull request flow. + +See https://github.com/ezyang/ghstack/issues/50 for more details, or +to complain to the ghstack authors.""" + ) + else: + needs_force = True + except ghstack.github.NotFoundError: + pass + + orig_ref, closed = lookup_pr_to_orig_ref_and_closed( + github, + owner=params["owner"], + name=params["name"], + number=params["number"], + ) + + if closed: + raise RuntimeError("PR is already closed, cannot land it!") + + if sh is None: + # Use CWD + sh = ghstack.shell.Shell() + + # Get up-to-date + sh.git("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) + ) + + # 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), + github_url=github_url, + ) + + # Switch working copy + try: + prev_ref = sh.git("symbolic-ref", "--short", "HEAD") + except RuntimeError: + prev_ref = sh.git("rev-parse", "HEAD") + + # If this fails, we don't have to reset + sh.git("checkout", f"{remote_name}/{default_branch}") + + try: + # Compute the metadata for each commit + stack_orig_refs: List[Tuple[str, PullRequestResolved]] = [] + for s in stack: + pr_resolved = s.pull_request_resolved + # 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( + github, + owner=pr_resolved.owner, + name=pr_resolved.repo, + number=pr_resolved.number, + ) + if closed and not force: + continue + stack_orig_refs.append((ref, pr_resolved)) + + # OK, actually do the land now + for orig_ref, _ in stack_orig_refs: + try: + sh.git("cherry-pick", f"{remote_name}/{orig_ref}") + except BaseException: + sh.git("cherry-pick", "--abort") + raise + + # All good! Push! + maybe_force_arg = [] + if needs_force: + maybe_force_arg = ["--force-with-lease"] + sh.git( + "push", *maybe_force_arg, remote_name, f"HEAD:refs/heads/{default_branch}" + ) + + # Advance base to head to "close" the PR for all PRs. + # This happens after the cherry-pick and push, because the cherry-picks + # can fail (merge conflict) and the push can also fail (race condition) + + # TODO: It might be helpful to advance orig to reflect the true + # state of upstream at the time we are doing the land, and then + # directly *merge* head into base, so that the PR accurately + # reflects what we ACTUALLY merged to master, as opposed to + # this synthetic thing I'm doing right now just to make it look + # like the PR got closed + + for orig_ref, pr_resolved in stack_orig_refs: + # 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( + "push", remote_name, f"{remote_name}/{head_ref}:refs/heads/{base_ref}" + ) + github.notify_merged(pr_resolved) + + # Delete the branches + for orig_ref, _ in stack_orig_refs: + # TODO: regex here so janky + 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) + 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) + except RuntimeError: + # Whatever, keep going + logging.warning("Failed to delete branch, continuing", exc_info=True) + + finally: + sh.git("checkout", prev_ref) diff --git a/src/ghstack/ghstack/logs.py b/src/ghstack/ghstack/logs.py new file mode 100644 index 0000000..ec7af4c --- /dev/null +++ b/src/ghstack/ghstack/logs.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 + +import contextlib +import datetime +import functools +import logging +import os +import re +import shutil +import subprocess +import sys +import uuid +from typing import Dict, Iterator, Optional + +DATETIME_FORMAT = "%Y-%m-%d_%Hh%Mm%Ss" + + +RE_LOG_DIRNAME = re.compile( + r"(\d{4}-\d\d-\d\d_\d\dh\d\dm\d\ds)_" r"[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}" +) + + +class Formatter(logging.Formatter): + redactions: Dict[str, str] + + def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None): + super().__init__(fmt, datefmt) + self.redactions = {} + + # Remove sensitive information from URLs + def _filter(self, s: str) -> str: + s = re.sub(r":\/\/(.*?)\@", r"://:@", s) + for needle, replace in self.redactions.items(): + s = s.replace(needle, replace) + return s + + def formatMessage(self, record: logging.LogRecord) -> str: + if record.levelno == logging.INFO or record.levelno == logging.DEBUG: + # Log INFO/DEBUG without any adornment + return record.getMessage() + else: + # I'm not sure why, but formatMessage doesn't show up + # even though it's in the typeshed for Python >3 + return super().formatMessage(record) # type: ignore + + def format(self, record: logging.LogRecord) -> str: + return self._filter(super().format(record)) + + # Redact specific strings; e.g., authorization tokens. This won't + # retroactively redact stuff you've already leaked, so make sure + # you redact things as soon as possible + def redact(self, needle: str, replace: str = "") -> None: + # Don't redact empty strings; this will lead to something + # that looks like str... + if needle == "": + return + self.redactions[needle] = replace + + +formatter = Formatter(fmt="%(levelname)s: %(message)s", datefmt="") + + +@contextlib.contextmanager +def manager(*, debug: bool = False) -> Iterator[None]: + # TCB code to setup logging. If a failure starts here we won't + # be able to save the user in a reasonable way. + + # Logging structure: there is one logger (the root logger) + # and in processes all events. There are two handlers: + # stderr (INFO) and file handler (DEBUG). + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + console_handler = logging.StreamHandler() + if debug: + console_handler.setLevel(logging.DEBUG) + else: + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + log_file = os.path.join(run_dir(), "ghstack.log") + + file_handler = logging.FileHandler(log_file) + # TODO: Hypothetically, it is better if we log the timestamp. + # But I personally feel the timestamps gunk up the log info + # for not much benefit (since we're not really going to be + # in the business of debugging performance bugs, for which + # timestamps would really be helpful.) Perhaps reconsider + # at some point based on how useful this information actually is. + # + # If you ever switch this, make sure to preserve redaction + # logic... + file_handler.setFormatter(formatter) + # file_handler.setFormatter(logging.Formatter( + # fmt="[%(asctime)s] [%(levelname)8s] %(message)s")) + root_logger.addHandler(file_handler) + + record_argv() + + try: + # Do logging rotation + rotate() + + yield + + except Exception as e: + logging.exception("Fatal exception") + record_exception(e) + sys.exit(1) + + +@functools.lru_cache() +def base_dir() -> str: + # Don't use shell here as we are not allowed to log yet! + try: + meta_dir = subprocess.run( + ("git", "rev-parse", "--git-dir"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + encoding="utf-8", + ).stdout.rstrip() + except subprocess.CalledProcessError: + meta_dir = os.path.join( + subprocess.run( + ("hg", "root"), stdout=subprocess.PIPE, encoding="utf-8", check=True + ).stdout.rstrip(), + ".hg", + ) + + base_dir = os.path.join(meta_dir, "ghstack", "log") + + try: + os.makedirs(base_dir) + except FileExistsError: + pass + + return base_dir + + +# Naughty, "run it once and save" memoizing +@functools.lru_cache() +def run_dir() -> str: + # NB: respects timezone + cur_dir = os.path.join( + base_dir(), + "{}_{}".format(datetime.datetime.now().strftime(DATETIME_FORMAT), uuid.uuid1()), + ) + + try: + os.makedirs(cur_dir) + except FileExistsError: + pass + + return cur_dir + + +def record_exception(e: BaseException) -> None: + with open(os.path.join(run_dir(), "exception"), "w") as f: + f.write(type(e).__name__) + + +@functools.lru_cache() +def record_argv() -> None: + with open(os.path.join(run_dir(), "argv"), "w") as f: + f.write(subprocess.list2cmdline(sys.argv)) + + +def record_status(status: str) -> None: + with open(os.path.join(run_dir(), "status"), "w") as f: + f.write(status) + + +def rotate() -> None: + log_base = base_dir() + old_logs = os.listdir(log_base) + old_logs.sort(reverse=True) + for stale_log in old_logs[1000:]: + # Sanity check that it looks like a log + assert RE_LOG_DIRNAME.fullmatch(stale_log) + shutil.rmtree(os.path.join(log_base, stale_log)) diff --git a/src/ghstack/ghstack/py.typed b/src/ghstack/ghstack/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/ghstack/ghstack/rage.py b/src/ghstack/ghstack/rage.py new file mode 100644 index 0000000..d0c9fe8 --- /dev/null +++ b/src/ghstack/ghstack/rage.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +import datetime +import os +import tempfile +from typing import Dict, NewType + +import ghstack +import ghstack.logs + +RawIndex = NewType("RawIndex", int) +FilteredIndex = NewType("FilteredIndex", int) + + +def get_argv(log_dir: str) -> str: + argv = "Unknown" + argv_fn = os.path.join(log_dir, "argv") + if os.path.exists(argv_fn): + with open(argv_fn, "r") as f: + argv = f.read().rstrip() + return argv + + +def get_status(log_dir: str) -> str: + status = "" + status_fn = os.path.join(log_dir, "status") + if os.path.exists(status_fn): + with open(status_fn, "r") as f: + status = f.read().rstrip() + return status + + +def main(latest: bool = False) -> None: + + log_base = ghstack.logs.base_dir() + logs = os.listdir(log_base) + logs.sort(reverse=True) + + filtered_mapping: Dict[FilteredIndex, RawIndex] = {} + + selected_index: FilteredIndex = FilteredIndex(0) + next_index: FilteredIndex = FilteredIndex(0) + if not latest: + print("Which invocation would you like to report?") + print() + for i, fn in enumerate(logs): + if next_index > 10: + break + + raw_index = RawIndex(i) + log_dir = os.path.join(log_base, fn) + + # Filter out rage + # NB: This doesn't have to be 100% sound; just need to be + # enough to good enough to filter out the majority of cases + argv = get_argv(log_dir) + argv_list = argv.split() + + if len(argv_list) >= 2 and argv_list[1] == "rage": + continue + + if len(argv_list) >= 1: + argv_list[0] = os.path.basename(argv_list[0]) + + argv = " ".join(argv_list) + + status = get_status(log_dir) + if status: + at_status = " at {}".format(status) + else: + at_status = "" + + cur_index = next_index + next_index = FilteredIndex(next_index + 1) + + filtered_mapping[cur_index] = raw_index + + m = ghstack.logs.RE_LOG_DIRNAME.fullmatch(fn) + if m: + date = ( + datetime.datetime.strptime(m.group(1), ghstack.logs.DATETIME_FORMAT) + .astimezone(tz=None) + .strftime("%a %b %d %H:%M:%S %Z") + ) + else: + date = "Unknown" + exception = "Succeeded" + exception_fn = os.path.join(log_base, fn, "exception") + if os.path.exists(exception_fn): + with open(exception_fn, "r") as f: + exception = "Failed with: " + f.read().rstrip() + + print( + "{:<5} {} [{}] {}{}".format( + "[{}].".format(cur_index), date, argv, exception, at_status + ) + ) + print() + selected_index = FilteredIndex( + int(input("(input individual number, for example 1 or 2)\n")) + ) + + log_dir = os.path.join(log_base, logs[filtered_mapping[selected_index]]) + + print() + print("Writing report, please wait...") + with tempfile.NamedTemporaryFile( + mode="w", suffix=".log", prefix="ghstack", delete=False + ) as g: + g.write("version: {}\n".format(ghstack.__version__)) + g.write("command: {}\n".format(get_argv(log_dir))) + g.write("status: {}\n".format(get_status(log_dir))) + g.write("\n") + log_fn = os.path.join(log_dir, "ghstack.log") + if os.path.exists(log_fn): + with open(log_fn) as log: + g.write(log.read()) + + print("=> Report written to {}".format(g.name)) + print("Please include this log with your bug report!") diff --git a/src/ghstack/ghstack/shell.py b/src/ghstack/ghstack/shell.py new file mode 100644 index 0000000..91d1f6a --- /dev/null +++ b/src/ghstack/ghstack/shell.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 + +import asyncio +import logging +import os +import shlex +import subprocess +import sys +from typing import Any, Dict, IO, Optional, overload, Sequence, Tuple, TypeVar, Union + +# Shell commands generally return str, but with exitcode=True +# they return a bool, and if stdout is piped straight to sys.stdout +# they return None. +_SHELL_RET = Union[bool, str, None] + + +_HANDLE = Union[None, int, IO[Any]] + + +def log_command(args: Sequence[str]) -> None: + """ + Given a command, print it in a both machine and human readable way. + + Args: + *args: the list of command line arguments you want to run + env: the dictionary of environment variable settings for the command + """ + cmd = " ".join(shlex.quote(arg) for arg in args) + logging.info("$ " + cmd) + + +K = TypeVar("K") + + +V = TypeVar("V") + + +def merge_dicts(x: Dict[K, V], y: Dict[K, V]) -> Dict[K, V]: + z = x.copy() + z.update(y) + return z + + +class Shell(object): + """ + An object representing a shell (e.g., the bash prompt in your + terminal), maintaining a concept of current working directory, and + also the necessary accoutrements for testing. + """ + + # Current working directory of shell. + cwd: str + + # Whether or not to suppress printing of command executed. + quiet: bool + + # Whether or not shell is in testing mode; some commands are made + # more deterministic in this case. + testing: bool + + # The current Unix timestamp. Only used during testing mode. + testing_time: int + + def __init__( + self, quiet: bool = False, cwd: Optional[str] = None, testing: bool = False + ): + """ + Args: + cwd: Current working directory of the shell. Pass None to + initialize to the current cwd of the current process. + quiet: If True, suppress printing out the command executed + by the shell. By default, we print out commands for ease + of debugging. Quiet is most useful for non-mutating + shell commands. + testing: If True, operate in testing mode. Testing mode + enables features which make the outputs of commands more + deterministic; e.g., it sets a number of environment + variables for Git. + """ + self.cwd = cwd if cwd else os.getcwd() + self.quiet = quiet + 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: + """ + Run a command specified by args, and return string representing + the stdout of the run command, raising an error if exit code + was nonzero (unless exitcode kwarg is specified; see below). + + Args: + *args: the list of command line arguments to run + env: any extra environment variables to set when running the + command. Environment variables set this way are ADDITIVE + (unlike subprocess default) + stderr: where to pipe stderr; by default, we pipe it straight + to this process's stderr + input: string value to pass stdin. This is mutually exclusive + with stdin + stdin: where to pipe stdin from. This is mutually exclusive + with input + stdout: where to pipe stdout; by default, we capture the stdout + and return it + exitcode: if True, return a bool rather than string, specifying + whether or not the process successfully returned with exit + code 0. We never raise an exception when this is True. + """ + assert not (stdin and input) + if tick: + self.test_tick() + if input: + stdin = subprocess.PIPE + if not self.quiet: + log_command(args) + if env is not None: + env = merge_dicts(dict(os.environ), env) + + # The things we do for logging... + # + # - I didn't make a PTY, so programs are going to give + # output assuming there isn't a terminal at the other + # end. This is less nice for direct terminal use, but + # it's better for logging (since we get to dispense + # with the control codes). + # + # - We assume line buffering. This is kind of silly but + # we need to assume *some* sort of buffering with the + # stream API. + + async def process_stream( + proc_stream: asyncio.StreamReader, setting: _HANDLE, default_stream: IO[str] + ) -> bytes: + output = [] + while True: + try: + line = await proc_stream.readuntil() + except asyncio.LimitOverrunError as e: + line = await proc_stream.readexactly(e.consumed) + except asyncio.IncompleteReadError as e: + line = e.partial + if not line: + break + output.append(line) + if setting == subprocess.PIPE: + pass + elif setting == subprocess.STDOUT: + sys.stdout.buffer.write(line) + elif isinstance(setting, int): + os.write(setting, line) + elif setting is None: + # Sigh. See https://stackoverflow.com/questions/55681488/python-3-write-binary-to-stdout-respecting-buffering + default_stream.write(line.decode("utf-8")) + else: + # NB: don't use setting.write directly, that will + # not properly handle binary. This gives us + # "parity" with the normal subprocess implementation + os.write(setting.fileno(), line) + return b"".join(output) + + async def feed_input(stdin_writer: Optional[asyncio.StreamWriter]) -> None: + if stdin_writer is None: + return + if not input: + return + stdin_writer.write(input.encode("utf-8")) + await stdin_writer.drain() + stdin_writer.close() + + async def run() -> Tuple[int, bytes, bytes]: + proc = await asyncio.create_subprocess_exec( + *args, + stdin=stdin, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=self.cwd, + env=env, + ) + assert proc.stdout is not None + assert proc.stderr is not None + _, out, err, _ = await asyncio.gather( + feed_input(proc.stdin), + process_stream(proc.stdout, stdout, sys.stdout), + process_stream(proc.stderr, stderr, sys.stderr), + proc.wait(), + ) + assert proc.returncode is not None + return (proc.returncode, out, err) + + loop = asyncio.get_event_loop() + returncode, out, err = loop.run_until_complete(run()) + + def decode(b: bytes) -> str: + return ( + b.decode(errors="backslashreplace") + .replace("\0", "\\0") + .replace("\r\n", "\n") + ) + + # NB: Not debug; we always want to show this to user. + if err: + logging.debug("# stderr:\n" + decode(err)) + if out: + logging.debug(("# stdout:\n" if err else "") + decode(out)) + + if exitcode: + logging.debug("Exit code: {}".format(returncode)) + return returncode == 0 + if returncode != 0: + raise RuntimeError( + "{} failed with exit code {}".format(" ".join(args), returncode) + ) + + if stdout == subprocess.PIPE: + return out.decode() # do a strict decode for actual return + else: + return None + + def _maybe_rstrip(self, s: _SHELL_RET) -> _SHELL_RET: + if isinstance(s, str): + return s.rstrip() + else: + return s + + @overload # noqa: F811 + def git(self, *args: str) -> str: ... + + @overload # noqa: F811 + def git(self, *args: str, input: str) -> str: ... + + @overload # noqa: F811 + def git(self, *args: str, input: str, env: Dict[str, str]) -> str: ... + + @overload # noqa: F811 + def git(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... + + 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() + """ + env = kwargs.setdefault("env", {}) + # For git hooks to detect execution inside ghstack + env.setdefault("GHSTACK", "1") + # For dealing with https://github.com/ezyang/ghstack/issues/174 + env.setdefault( + "GIT_TERMINAL_PROMPT", os.environ.get("GIT_TERMINAL_PROMPT", "0") + ) + # Some envvars to make things a little more script mode nice + if self.testing: + env.setdefault("EDITOR", ":") + env.setdefault("GIT_MERGE_AUTOEDIT", "no") + env.setdefault("LANG", "C") + env.setdefault("LC_ALL", "C") + env.setdefault("PAGER", "cat") + env.setdefault("TZ", "UTC") + env.setdefault("TERM", "dumb") + # These are important so we get deterministic commit times + env.setdefault("GIT_AUTHOR_EMAIL", "author@example.com") + env.setdefault("GIT_AUTHOR_NAME", "A U Thor") + env.setdefault("GIT_COMMITTER_EMAIL", "committer@example.com") + env.setdefault("GIT_COMMITTER_NAME", "C O Mitter") + env.setdefault("GIT_COMMITTER_DATE", "{} -0700".format(self.testing_time)) + env.setdefault("GIT_AUTHOR_DATE", "{} -0700".format(self.testing_time)) + if "stderr" not in kwargs: + kwargs["stderr"] = subprocess.PIPE + + return self._maybe_rstrip(self.sh(*(("git",) + args), **kwargs)) + + @overload # noqa: F811 + def hg(self, *args: str) -> str: ... + + @overload # noqa: F811 + def hg(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._maybe_rstrip(self.sh(*(("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() + """ + + kwargs.setdefault("stdout", sys.stderr) + + return self._maybe_rstrip(self.sh(*(("jf",) + args), **kwargs)) + + def test_tick(self) -> None: + """ + Increase the current time. Useful when testing is True. + """ + self.testing_time += 60 + + def open(self, fn: str, mode: str) -> IO[Any]: + """ + Open a file, relative to the current working directory. + + Args: + fn: filename to open + mode: mode to open the file as + """ + return open(os.path.join(self.cwd, fn), mode) + + def cd(self, d: str) -> None: + """ + Change the current working directory. + + Args: + d: directory to change to + """ + self.cwd = os.path.join(self.cwd, d) diff --git a/src/ghstack/ghstack/status.py b/src/ghstack/ghstack/status.py new file mode 100644 index 0000000..313f5bf --- /dev/null +++ b/src/ghstack/ghstack/status.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import logging +import re + +import aiohttp +from typing_extensions import TypedDict + +import ghstack.circleci +import ghstack.github +import ghstack.github_utils + +RE_CIRCLECI_URL = re.compile(r"^https://circleci.com/gh/pytorch/pytorch/([0-9]+)") + + +def strip_sccache(x: str) -> str: + sccache_marker = "=================== sccache compilation log ===================" + marker_pos = x.rfind(sccache_marker) + newline_before_marker_pos = x.rfind("\n", 0, marker_pos) + return x[:newline_before_marker_pos] + + +async def main( + pull_request: str, # noqa: C901 + github: ghstack.github.GitHubEndpoint, + circleci: ghstack.circleci.CircleCIEndpoint, +) -> None: + + # Game plan: + # 1. Query GitHub to find out what the current statuses are + # (TODO: if we got rate limited we'll miss stuff) + # 2. For each status in parallel: + # a. Query CircleCI for job status + # b. (Future work) Query output_url to get log information + # (it's gzip'ed) + # + # For now: + # - Print if the job actually ran, or was skipped + # - Easy way to determine: check if "Should run job after + # checkout" is last step + # - I inspected circleci.get('project/github/pytorch/pytorch/1773555') + # to see if there were other options, there did not appear + # 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) + + ContextPayload = TypedDict( + "ContextPayload", + { + "context": str, + "state": str, + "targetUrl": str, + }, + ) + r = github.graphql( + """ + query ($name: String!, $owner: String!, $number: Int!) { + repository(name: $name, owner: $owner) { + pullRequest(number: $number) { + commits(last: 1) { + nodes { + commit { + status { + contexts { + context + state + targetUrl + } + } + } + } + } + } + } + } + """, + **params, + ) + contexts = r["data"]["repository"]["pullRequest"]["commits"]["nodes"][0]["commit"][ + "status" + ]["contexts"] + + async def process_context(context: ContextPayload) -> str: + text = "" + if "circleci" in context["context"]: + m = RE_CIRCLECI_URL.match(context["targetUrl"]) + if not m: + logging.warning( + "Malformed CircleCI URL {}".format(context["targetUrl"]) + ) + return "INTERNAL ERROR {}".format(context["context"]) + buildid = m.group(1) + r = await circleci.get( + "project/github/{name}/{owner}/{buildid}".format( + buildid=buildid, **params + ) + ) + if context["state"] not in {"SUCCESS", "PENDING"}: + state = context["state"] + else: + if r["failed"]: + state = "FAILURE" + elif r["canceled"]: + state = "CANCELED" + elif "Should Run Job" in r["steps"][-1]["name"]: + state = "SKIPPED" + else: + state = "SUCCESS" + if state == "FAILURE": + async with aiohttp.request( + "get", r["steps"][-1]["actions"][-1]["output_url"] + ) as resp: + log_json = await resp.json() + buf = [] + for e in log_json: + buf.append(e["message"]) + text = "\n" + strip_sccache("\n".join(buf)) + text = text[-1500:] + else: + state = context["state"] + + if state == "SUCCESS": + state = "✅" + elif state == "SKIPPED": + state = "❔" + elif state == "CANCELED": + state = "💜" + elif state == "PENDING": + state = "🚸" + elif state == "FAILURE": + state = "❌" + name = context["context"] + url = context["targetUrl"] + url = url.replace( + "?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link", + "", + ) + return "{} {} {}{}".format(state, name.ljust(70), url, text) + + results = await asyncio.gather( + *[asyncio.ensure_future(process_context(c)) for c in contexts] + ) + print("\n".join(sorted(results))) diff --git a/src/ghstack/ghstack/submit.py b/src/ghstack/ghstack/submit.py new file mode 100644 index 0000000..797d00f --- /dev/null +++ b/src/ghstack/ghstack/submit.py @@ -0,0 +1,1849 @@ +#!/usr/bin/env python3 + +import dataclasses +import itertools +import logging +import os +import re +from dataclasses import dataclass +from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple + +import ghstack +import ghstack.git +import ghstack.github +import ghstack.github_utils +import ghstack.gpg_sign +import ghstack.logs +import ghstack.shell +import ghstack.trailers +from ghstack.types import GhNumber, GitCommitHash, GitHubNumber, GitHubRepositoryId + +# Either "base", "head" or "orig"; which of the ghstack generated +# branches this diff corresponds to +# For direct ghstack, either "next", "head" or "orig" +BranchKind = str + + +@dataclass(frozen=True) +class GhCommit: + commit_id: GitCommitHash + tree: str + + +# Commit can be None if this is a completely fresh PR +@dataclass +class GhBranch: + commit: Optional[GhCommit] = None + updated: bool = False + + def update(self, val: GhCommit) -> None: + self.commit = val + self.updated = True + + +@dataclass +class GhBranches: + # What Git commit hash we should push to what branch. + # The orig branch is populated later + orig: GhBranch = dataclasses.field(default_factory=GhBranch) + head: GhBranch = dataclasses.field(default_factory=GhBranch) + base: GhBranch = dataclasses.field(default_factory=GhBranch) + next: GhBranch = dataclasses.field(default_factory=GhBranch) + + def to_list(self) -> List[Tuple[GitCommitHash, BranchKind]]: + r = [] + if self.orig.updated: + assert self.orig.commit is not None + r.append((self.orig.commit.commit_id, "orig")) + if self.next.updated: + assert self.next.commit is not None + r.append((self.next.commit.commit_id, "next")) + if self.base.updated: + assert self.base.commit is not None + r.append((self.base.commit.commit_id, "base")) + if self.head.updated: + assert self.head.commit is not None + r.append((self.head.commit.commit_id, "head")) + return r + + def __iter__(self) -> Iterator[Tuple[GitCommitHash, BranchKind]]: + return iter(self.to_list()) + + def __bool__(self) -> bool: + return bool(self.to_list()) + + def clear(self) -> None: + self.orig.updated = False + self.head.updated = False + self.base.updated = False + self.next.updated = False + + +@dataclass(frozen=True) +class PreBranchState: + # NB: these do not necessarily coincide with head/base branches. + # In particular, in direct mode, the base commit will typically be + # another head branch, or the upstream main branch itself. + base_commit_id: GitCommitHash + head_commit_id: GitCommitHash + + +# Ya, sometimes we get carriage returns. Crazy right? +RE_STACK = re.compile(r"Stack.*:\r?\n(\* [^\r\n]+\r?\n)+") + + +# NB: This regex is fuzzy because the D1234567 identifier is typically +# linkified. +RE_DIFF_REV = re.compile(r"^Differential Revision:.+?(D[0-9]+)", re.MULTILINE) + + +# Suppose that you have submitted a commit to GitHub, and that commit's +# tree was AAA. The ghstack-source-id of your local commit after this +# submit is AAA. When you submit a new change on top of this, we check +# that the source id associated with your orig commit agrees with what's +# recorded in GitHub: this lets us know that you are "up-to-date" with +# what was stored on GitHub. Then, we update the commit message on your +# local commit to record a new ghstack-source-id and push it to orig. +# +# We must store this in the orig commit as we have no other mechanism of +# attaching information to a commit in question. We don't store this in +# the pull request body as there isn't really any need to do so. +RE_GHSTACK_SOURCE_ID = re.compile(r"^ghstack-source-id: (.+)\n?", re.MULTILINE) + + +# When we make a GitHub PR using --direct, we submit an extra comment which +# contains the links to the rest of the PRs in the stack. We don't put this +# inside the pull request body, because if you squash merge the PR, that body +# gets put into the commit message, but the stack information is just line +# noise and shouldn't go there. +# +# We can technically find the ghstack commit by querying GitHub API for all +# comments, but this is a more efficient way of getting it. +RE_GHSTACK_COMMENT_ID = re.compile(r"^ghstack-comment-id: (.+)\n?", re.MULTILINE) + + +# repo layout: +# - gh/username/23/head -- what we think GitHub's current tip for commit is +# - gh/username/23/base -- what we think base commit for commit is +# - gh/username/23/orig -- the "clean" commit history, i.e., what we're +# rebasing, what you'd like to cherry-pick (???) +# (Maybe this isn't necessary, because you can +# get the "whole" diff from GitHub? What about +# commit description?) +# +# +# In direct mode, there is no base branch, instead: +# +# - gh/username/23/next -- staging ground for commits that must exist +# for later PRs in the stack to merge against, but should not be shown +# for the PR itself (because that PR was not submitted) + + +def branch(username: str, ghnum: GhNumber, kind: BranchKind) -> GitCommitHash: + return GitCommitHash("gh/{}/{}/{}".format(username, ghnum, kind)) + + +def branch_base(username: str, ghnum: GhNumber) -> GitCommitHash: + return branch(username, ghnum, "base") + + +def branch_head(username: str, ghnum: GhNumber) -> GitCommitHash: + return branch(username, ghnum, "head") + + +def branch_orig(username: str, ghnum: GhNumber) -> GitCommitHash: + return branch(username, ghnum, "orig") + + +def branch_next(username: str, ghnum: GhNumber) -> GitCommitHash: + return branch(username, ghnum, "next") + + +RE_MENTION = re.compile(r"(? str: + return RE_MENTION.sub(r"\1", body) + + +STACK_HEADER = f"Stack from [ghstack](https://github.com/ezyang/ghstack/tree/{ghstack.__version__}) (oldest at bottom)" + + +def starts_with_bullet(body: str) -> bool: + """ + Returns True if the string in question begins with a Markdown + bullet list + """ + return bool(re.match(r"^[\s\t]*[*\-+][\s\t]+", body)) + + +@dataclass +class DiffWithGitHubMetadata: + diff: ghstack.diff.Diff + number: GitHubNumber + username: str + # Really ought not to be optional, but for BC reasons it might be + remote_source_id: Optional[str] + # Guaranteed to be set for --direct PRs + comment_id: Optional[int] + title: str + body: str + closed: bool + ghnum: GhNumber + pull_request_resolved: ghstack.diff.PullRequestResolved + head_ref: str + base_ref: str + + +# Metadata describing a diff we submitted to GitHub +@dataclass +class DiffMeta: + elab_diff: DiffWithGitHubMetadata + # The commit message to put on the orig commit + commit_msg: str + + push_branches: GhBranches + # A human-readable string like 'Created' which describes what + # happened to this pull request + what: str + + # The name of the branch that should be targeted + base: str + + @property + def pr_url(self) -> str: + return self.elab_diff.pull_request_resolved.url() + + @property + def title(self) -> str: + return self.elab_diff.title + + @property + def number(self) -> GitHubNumber: + return self.elab_diff.number + + @property + def body(self) -> str: + return self.elab_diff.body + + @property + def username(self) -> str: + return self.elab_diff.username + + @property + def ghnum(self) -> GhNumber: + return self.elab_diff.ghnum + + @property + def closed(self) -> bool: + return self.elab_diff.closed + + @property + def orig(self) -> GitCommitHash: + assert self.push_branches.orig.commit is not None + return self.push_branches.orig.commit.commit_id + + @property + def head(self) -> GitCommitHash: + assert self.push_branches.head.commit is not None + return self.push_branches.head.commit.commit_id + + @property + def next(self) -> GitCommitHash: + assert self.push_branches.next.commit is not None + return self.push_branches.next.commit.commit_id + + +def main(**kwargs: Any) -> List[DiffMeta]: + submitter = Submitter(**kwargs) + return submitter.run() + + +def all_branches(username: str, ghnum: GhNumber) -> Tuple[str, str, str]: + return ( + branch_base(username, ghnum), + branch_head(username, ghnum), + branch_orig(username, ghnum), + ) + + +def push_spec(commit: GitCommitHash, branch: str) -> str: + return "{}:refs/heads/{}".format(commit, branch) + + +@dataclass(frozen=True) +class Submitter: + """ + A class responsible for managing the environment associated + with submitting PRs at GitHub. + """ + + # --------------------------- + # Direct arguments to submit + + # Message describing the update to the stack that was done + msg: Optional[str] + + # GitHub username who is doing the submitting + username: str + + # Endpoint to access GitHub + github: ghstack.github.GitHubEndpoint + + # Clobber existing PR description with local commit message + update_fields: bool = False + + # Shell inside git checkout that we are submitting + sh: ghstack.shell.Shell = dataclasses.field(default_factory=ghstack.shell.Shell) + + # String used to describe the stack in question + stack_header: str = STACK_HEADER + + # Owner of the repository we are submitting to. Usually 'pytorch' + # Presents as repo_owner kwarg in main + repo_owner_opt: Optional[str] = None + + # Name of the repository we are submitting to. Usually 'pytorch' + # Presents as repo_name kwarg in main + repo_name_opt: Optional[str] = None + + # allow configuration to optionally define repo details + repo_is_fork_opt: Optional[str] = None + repo_id_opt: Optional[str] = None + repo_default_branch_opt: Optional[str] = None + + # Print only PR URL to stdout + short: bool = False + + # Force an update to GitHub, even if we think that your local copy + # is stale. + force: bool = False + + # Do not skip unchanged diffs + no_skip: bool = False + + # Create the PR in draft mode if it is going to be created (and not updated). + draft: bool = False + + # Github url (normally github.com) + github_url: str = "github.com" + + # Name of the upstream remote (normally origin) + remote_name: str = "origin" + + base_opt: Optional[str] = None + + revs: Sequence[str] = () + + # Controls rev parse behavior, whether or not to submit a stack + # of commits or only one commit individually + stack: bool = True + + # Check that invariants are upheld during execution + check_invariants: bool = False + + # Instead of merging into base branch, merge directly into the appropriate + # main or head branch. Change merge targets appropriately as PRs get + # merged. If None, infer whether or not the PR should be direct or not. + direct_opt: Optional[bool] = None + + # ~~~~~~~~~~~~~~~~~~~~~~~~ + # Computed in post init + + # GraphQL ID of the repository + repo_id: GitHubRepositoryId = dataclasses.field(init=False) + + repo_owner: str = dataclasses.field(init=False) + + repo_name: str = dataclasses.field(init=False) + + base: str = dataclasses.field(init=False) + + direct: bool = dataclasses.field(init=False) + + # ~~~~~~~~~~~~~~~~~~~~~~~~ + # Mutable state; TODO: remove me + + # List of input diffs which we ignored (i.e., treated as if they + # did not exist on the stack at all), because they were associated + # with a patch that contains no changes. GhNumber may be false + # if the diff was never associated with a PR. + ignored_diffs: List[Tuple[ghstack.diff.Diff, Optional[DiffWithGitHubMetadata]]] = ( + dataclasses.field(default_factory=list) + ) + + # Set of seen ghnums + seen_ghnums: Set[Tuple[str, GhNumber]] = dataclasses.field(default_factory=set) + + # ~~~~~~~~~~~~~~~~~~~~~~~~ + # Post initialization + + def __post_init__(self) -> None: + repo = {} + if ( + self.repo_id_opt is not None + and self.repo_owner_opt is not None + and self.repo_name_opt is not None + and self.repo_is_fork_opt is not None + and self.repo_default_branch_opt is not None + ): + repo["is_fork"] = self.repo_is_fork_opt + repo["id"] = self.repo_id_opt + repo["default_branch"] = self.repo_default_branch_opt + repo["name_with_owner"] = {} + repo["name_with_owner"]["owner"] = self.repo_owner_opt + repo["name_with_owner"]["name"] = self.repo_name_opt + else: + # Network call in the constructor, help me father, for I have sinned + repo = ghstack.github_utils.get_github_repo_info( + github=self.github, + sh=self.sh, + repo_owner=self.repo_owner_opt, + repo_name=self.repo_name_opt, + github_url=self.github_url, + remote_name=self.remote_name, + ) + object.__setattr__(self, "repo_owner", repo["name_with_owner"]["owner"]) + object.__setattr__(self, "repo_name", repo["name_with_owner"]["name"]) + + if repo["is_fork"]: + raise RuntimeError( + "Cowardly refusing to upload diffs to a repository that is a " + "fork. ghstack expects '{}' of your Git checkout to point " + "to the upstream repository in question. If your checkout does " + "not comply, please either adjust your remotes (by editing " + ".git/config) or change the 'remote_name' field in your .ghstackrc " + "file to point to the correct remote. If this message is in " + "error, please register your complaint on GitHub issues (or edit " + "this line to delete the check above).".format(self.remote_name) + ) + object.__setattr__(self, "repo_id", repo["id"]) + if self.base_opt is not None: + default_branch = self.base_opt + else: + default_branch = repo["default_branch"] + + object.__setattr__(self, "base", default_branch) + + # Check if direct should be used, if the user didn't explicitly + # specify an option + direct = self.direct_opt + if direct is None: + direct_r = self.sh.git( + "cat-file", "-e", "HEAD:.github/ghstack_direct", exitcode=True + ) + assert isinstance(direct_r, bool) + direct = direct_r + + object.__setattr__(self, "direct", direct) + + # ~~~~~~~~~~~~~~~~~~~~~~~~ + # The main algorithm + + def run(self) -> List[DiffMeta]: + self.fetch() + + commits_to_submit_and_boundary = self.parse_revs() + + commits_to_submit = [ + d for d in commits_to_submit_and_boundary if not d.boundary + ] + + # NB: A little bit of redundant parsing here, because we will re-parse + # commits that we had already parsed in commits_to_submit, and we will + # 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( + "rev-list", + "--boundary", + "--header", + "--topo-order", + # Get all commits reachable from HEAD... + "HEAD", + # ...as well as all the commits we are going to submit... + *[c.commit_id for c in commits_to_submit], + # ...but we don't need any commits that aren't draft + f"^{self.remote_name}/{self.base}", + ) + ) + + commits_to_rebase = [ + d for d in commits_to_rebase_and_boundary if not d.boundary + ] + + # NB: commits_to_rebase always contains all diffs to submit (because + # we always have to generate orig commits for submitted diffs.) + # However, commits_to_submit does not necessarily contain + # diffs_to_rebase. If you ask to submit only a prefix of your current + # stack, the suffix is not to be submitted, but it needs to be rebased + # (to, e.g., update the ghstack-source-id) + + commit_count = len(commits_to_submit) + + if commit_count == 0: + raise RuntimeError( + "There appears to be no commits to process, based on the revs you passed me." + ) + + # 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 + ) + + # 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] = {} + if self.check_invariants: + 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) + pre_branch_state_index[h.commit_id] = PreBranchState( + head_commit_id=GitCommitHash( + self.sh.git( + "rev-parse", f"{self.remote_name}/{ed.head_ref}" + ) + ), + base_commit_id=GitCommitHash( + self.sh.git( + "rev-parse", f"{self.remote_name}/{ed.base_ref}" + ) + ), + ) + + # NB: deduplicates + commit_index = { + h.commit_id: h + for h in itertools.chain( + commits_to_submit_and_boundary, commits_to_rebase_and_boundary + ) + } + diff_meta_index, rebase_index = self.prepare_updates( + commit_index, commits_to_submit, commits_to_rebase + ) + logging.debug("rebase_index = %s", rebase_index) + diffs_to_submit = [ + diff_meta_index[h.commit_id] + for h in commits_to_submit + if h.commit_id in diff_meta_index + ] + self.push_updates(diffs_to_submit) + if new_head := rebase_index.get( + old_head := GitCommitHash(self.sh.git("rev-parse", "HEAD")) + ): + self.sh.git("reset", "--soft", new_head) + # TODO: print out commit hashes for things we rebased but not accessible + # from HEAD + + if self.check_invariants: + 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( + h.commit_id, + new_orig, + pre_branch_state_index.get(h.commit_id), + ) + # 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( + "merge-base", "--is-ancestor", h.commit_id, old_head, exitcode=True + ): + assert new_head is not None + assert self.sh.git( + "merge-base", "--is-ancestor", new_orig, new_head, exitcode=True + ) + else: + if new_head is not None: + assert not self.sh.git( + "merge-base", + "--is-ancestor", + new_orig, + new_head, + exitcode=True, + ) + + # 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 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, + # because you only did minor changes to the commits below and + # you want to let the CI finish without those changes. + # See https://github.com/ezyang/ghstack/issues/165 + # + # 2. I want to submit a prefix of the stack, because I'm still working + # on the top of the stack and don't want to spam people with + # useless changes. See https://github.com/ezyang/ghstack/issues/101 + # + # If we use standard git log/rev-list style parsing, you get (2) by + # default because a single commit implies a reachability constraint. + # Specifying (1) is a bit inconvenient; you have to say something + # like `ghstack submit HEAD~..`. In particular, both (1) and (2) would like + # the meaning of `ghstack submit HEAD` to do different things (1 wants a single + # commit, whereas 2 wants everything reachable from the commit.) + # + # To resolve the ambiguity, we introduce a new command line argument + # --no-stack (analogous to the --stack argument on jf) which disables + # "stacky" behavior. With --no-stack, we only submit HEAD by default + # and you can also specify a specific commit to submit if you like + # (if this commit is not reachable from HEAD, we will tell you how + # to checkout the updated commit.) If you specify multiple commits, + # we will process each of them in turn. Ranges are not supported; use + # git rev-list to preprocess them into single commits first (in principle + # we could support this, but it would require determining if a REV was + # a range versus a commit, as different handling would be necessary + # in each case.) + # + # Without --no-stack, we use standard git rev-list semantics. Some of the + # more advanced spellings can be counterintuitive, but `ghstack submit X` + # is equivalent to checking out X and then performing ghstack (and then + # restacking HEAD on top, if necessary), and you can say `X..Y` + # (exclusive-inclusive) to specify a specific range of commits (oddly, + # `X..` will do what you expect, but `..Y` will almost always be empty.) + # But I expect this to be fairly niche. + # + # In both cases, we support submitting multiple commits, because the set + # of commits you specify affects what rebasing we do, which is sometimes + # not conveniently done by calling ghstack multiple times. + + # Interestingly, the default is the same whether it is --stack or + # --no-stack + revs = ("HEAD",) if not self.revs else self.revs + + # In jf, we determine whether or not we should consider a diff by checking + # if it is draft or not (only draft commits can be posted). Git doesn't + # have a directly analogous concept, so we need some other strategy. A + # simple approach is to inspect the base branch in the upstream + # repository, and exclude all commits which are reachable from it. + # We don't want to blast ALL remote branches into the list here though; + # it's possible the draft commits were pushed to the remote repo for + # unrelated reasons, and we don't want to treat them as non-draft if + # this happens! + + commits_to_submit_and_boundary = [] + if self.stack: + # Easy case, make rev-list do the hard work + commits_to_submit_and_boundary.extend( + ghstack.git.split_header( + self.sh.git( + "rev-list", + "--header", + "--topo-order", + "--boundary", + *revs, + f"^{self.remote_name}/{self.base}", + ), + ) + ) + else: + # Hard case, need to query rev-list repeatedly + for rev in revs: + # We still do rev-list as it gets us the parent commits + r = ghstack.git.split_header( + self.sh.git( + "rev-list", + "--header", + "--topo-order", + "--boundary", + f"{rev}~..{rev}", + f"^{self.remote_name}/{self.base}", + ), + ) + if not r: + raise RuntimeError( + f"{r} doesn't seem to be a commit that can be submitted!" + ) + # NB: There may be duplicate commits that are + # boundary/not-boundary, but once we generate commits_to_submit + # there should not be any dupes if rev was not duped + # TODO: check no dupe revs, though actually it's harmless + commits_to_submit_and_boundary.extend(r) + + return commits_to_submit_and_boundary + + 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], + ) -> 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 + submit_set = set(h.commit_id for h in commits_to_submit) + diff_meta_index: Dict[GitCommitHash, DiffMeta] = {} + rebase_index: Dict[GitCommitHash, GitCommitHash] = {} + for commit in reversed(commits_to_rebase): + submit = commit.commit_id in submit_set + parents = commit.parents + if len(parents) != 1: + raise RuntimeError( + "The commit {} has {} parents, which makes my head explode. " + "`git rebase -i` your diffs into a stack, then try again.".format( + commit.commit_id, len(parents) + ) + ) + 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) + diff_meta = self.process_commit( + parent_commit, + parent_diff_meta, + diff, + ( + self.elaborate_diff(diff) + if diff.pull_request_resolved is not None + else None + ), + submit, + ) + 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 + + 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 + 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 + if commit.author_email is not None: + env["GIT_AUTHOR_EMAIL"] = commit.author_email + + new_orig = GitCommitHash( + self.sh.git( + "commit-tree", + *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + "-p", + base_commit_id, + commit.tree, + input=commit_msg, + env=env, + ) + ) + + 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 + + return diff_meta_index, rebase_index + + def elaborate_diff( + self, diff: ghstack.diff.Diff, *, is_ghexport: bool = False + ) -> DiffWithGitHubMetadata: + """ + Query GitHub API for the current title, body and closed? status + of the pull request corresponding to a ghstack.diff.Diff. + """ + + assert diff.pull_request_resolved is not None + assert diff.pull_request_resolved.owner == self.repo_owner + 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: + 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. +""" + ) + + # TODO: Hmm, I'm not sure why this matches + m = re.match(r"gh/([^/]+)/([0-9]+)/head$", r["headRefName"]) + 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 + ) + ) + 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) + + # TODO: remote summary should be done earlier so we can use + # it to test if updates are necessary + + try: + rev_list = self.sh.git( + "rev-list", + "--max-count=1", + "--header", + self.remote_name + "/" + branch_orig(username, gh_number), + ) + except RuntimeError as e: + if r["closed"]: + raise RuntimeError( + f"Cannot ghstack a stack with closed PR #{number} 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." + ) from e + raise + remote_summary = ghstack.git.split_header(rev_list)[0] + m_remote_source_id = RE_GHSTACK_SOURCE_ID.search(remote_summary.commit_msg) + remote_source_id = m_remote_source_id.group(1) if m_remote_source_id else None + m_comment_id = RE_GHSTACK_COMMENT_ID.search(remote_summary.commit_msg) + comment_id = int(m_comment_id.group(1)) if m_comment_id else None + + return DiffWithGitHubMetadata( + diff=diff, + title=title, + body=pr_body, + closed=r["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"], + ) + + def process_commit( + self, + base: ghstack.git.CommitHeader, + base_diff_meta: Optional[DiffMeta], + diff: ghstack.diff.Diff, + elab_diff: Optional[DiffWithGitHubMetadata], + submit: bool, + ) -> Optional[DiffMeta]: + # Do not process poisoned commits + if "[ghstack-poisoned]" in diff.summary: + self._raise_poisoned() + + # Do not process closed commits + if elab_diff is not None and elab_diff.closed: + if self.direct: + self._raise_needs_rebase() + return None + + # Edge case: check if the commit is empty; if so skip submitting + if base.tree == diff.tree: + self._warn_empty(diff, elab_diff) + # Maybe it can just fall through here and make an empty PR fine + assert not self.direct, "empty commits with direct NYI" + 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() + self._sanity_check_ghnum(username, ghnum) + + # Create base/head commits if needed + push_branches, base_branch = self._create_non_orig_branches( + base, base_diff_meta, diff, elab_diff, username, ghnum, submit + ) + + # 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] + ) + push_branches.clear() + elab_diff = self._create_pull_request(diff, base_diff_meta, ghnum) + what = "Created" + new_pr = True + else: + if not push_branches: + what = "Skipped" + elif push_branches.head is None: + 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, + ) + + def _raise_poisoned(self) -> None: + raise RuntimeError( + """\ +This commit is poisoned: it is from a head or base branch--ghstack +cannot validly submit it. The most common situation for this to +happen is if you checked out the head branch of a pull request that was +previously submitted with ghstack (e.g., by using hub checkout). +Making modifications on the head branch is not supported; instead, +you should fetch the original commits in question by running: + +ghstack checkout $PR_URL + +Since we cannot proceed, ghstack will abort now. +""" + ) + + def _raise_needs_rebase(self) -> None: + raise RuntimeError( + """\ +ghstack --next requires all PRs in the stack to be open. One of your PRs +is closed (likely due to being merged). Please rebase to upstream and try again. +""" + ) + + def _warn_empty( + self, diff: ghstack.diff.Diff, elab_diff: Optional[DiffWithGitHubMetadata] + ) -> None: + self.ignored_diffs.append((diff, elab_diff)) + logging.warning( + "Skipping '{}', as the commit now has no changes".format(diff.title) + ) + + 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) + 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)) + + def _sanity_check_ghnum(self, username: str, ghnum: GhNumber) -> None: + if (username, ghnum) in self.seen_ghnums: + raise RuntimeError( + "Something very strange has happened: a commit for " + f"the gh/{username}/{ghnum} 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." + ) + self.seen_ghnums.add((username, ghnum)) + + def _update_source_id(self, summary: str, elab_diff: DiffWithGitHubMetadata) -> str: + m_local_source_id = RE_GHSTACK_SOURCE_ID.search(summary) + if m_local_source_id is None: + # This is for an already submitted PR, so there should + # already be a source id on it. But there isn't. + # For BC, just slap on a source ID. After BC is no longer + # needed, we can just error in this case; however, this + # situation is extremely likely to happen for preexisting + # stacks. + logging.warning( + "Local commit has no ghstack-source-id; assuming that it is " + "up-to-date with remote." + ) + summary = "{}\nghstack-source-id: {}".format( + summary, elab_diff.diff.source_id + ) + else: + local_source_id = m_local_source_id.group(1) + if elab_diff.remote_source_id is None: + # This should also be an error condition, but I suppose + # it can happen in the wild if a user had an aborted + # ghstack run, where they updated their head pointer to + # a copy with source IDs, but then we failed to push to + # orig. We should just go ahead and push in that case. + logging.warning( + "Remote commit has no ghstack-source-id; assuming that we are " + "up-to-date with remote." + ) + elif local_source_id != elab_diff.remote_source_id and not self.force: + logging.debug( + f"elab_diff.remote_source_id = {elab_diff.remote_source_id}" + ) + # TODO: have a 'ghstack pull' remediation for this case + raise RuntimeError( + "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 " + "{} and reapply them on top of an up-to-date commit from " + "GitHub.".format(local_source_id) + ) + summary = RE_GHSTACK_SOURCE_ID.sub( + "ghstack-source-id: {}\n".format(elab_diff.diff.source_id), summary + ) + return summary + + # NB: mutates GhBranch + 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) + ) + 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: + push_branches = GhBranches() + self._resolve_gh_branch("orig", push_branches.orig, username, ghnum) + self._resolve_gh_branch("head", push_branches.head, username, ghnum) + if self.direct: + self._resolve_gh_branch("next", push_branches.next, username, ghnum) + else: + self._resolve_gh_branch("base", push_branches.base, username, ghnum) + return push_branches + + def _create_non_orig_branches( + self, + base: ghstack.git.CommitHeader, + base_diff_meta: Optional[DiffMeta], + diff: ghstack.diff.Diff, + elab_diff: Optional[DiffWithGitHubMetadata], + username: str, + ghnum: GhNumber, + submit: bool, + ) -> Tuple[GhBranches, str]: + # How exactly do we submit a commit to GitHub? + # + # Here is the relevant state: + # - Local parent tree + # - Local commit tree + # - Remote base branch + # - Remote head branch + # + # Our job is to synchronize local with remote. Here are a few + # common situations: + # + # - Neither this commit nor any of the earlier commits were + # modified; everything is in sync. We want to do nothing in this + # case. + # + # - User updated top commit on stack, but none of the earlier commits. + # Here, we expect local parent tree to match remote base tree (BA), but + # local commit tree to mismatch remote head branch (A). We will push + # a new commit to head (A2), no merge necessary. + # + # BA + # \ + # A - A2 + # + # - User updated an earlier commit in the stack (it doesn't matter + # if the top commit is logically modified or not: it always counts as + # having been modified to resolve the merge.) We don't expect + # local parent tree to match remote base tree, so we must push a + # new base commit (BA2), and a merge commit (A2) on it. + # + # BA - BA2 + # \ \ + # A - A2 + # + # Notably, this must happen even if the local commit tree matches + # the remote head branch. A common situation this could occur is + # if we squash commits I and J into IJ (keeping J as the tree). + # Then for J we see: + # + # BJ - BJ2 + # \ \ + # J - BJ2 + # + # Where BJ contains I, but BJ2 does NOT contain I. The net result + # is the changes of I are included inside the BJ2 merge commit. + # + # First time submission proceeds similarly, except that we no longer + # need to create a parent pointer to the previous base/head. + # + # Note that, counterintuitively, the base of a diff has no + # relationship to the head of an earlier diff on the stack. This + # makes it possible to selectively only update one diff in a stack + # without updating any others. This also makes our handling uniform + # even if you rebase a commit backwards: you just see that the base + # is updated to also remove changes. + + if elab_diff is not None: + push_branches = self._resolve_gh_branches(username, ghnum) + else: + push_branches = GhBranches() + + # Initialize head arguments (as original head parent must come first + # in parents list) + head_args: List[str] = [] + if push_branches.head.commit is not None: + head_args.extend(("-p", push_branches.head.commit.commit_id)) + + # Create base commit if necessary + updated_base = False + if not self.direct: + base_branch = branch_base(username, ghnum) + if ( + push_branches.base.commit is None + or push_branches.base.commit.tree != base.tree + ): + # Base is not the same, perform base update + updated_base = True + base_args: List[str] = [] + if push_branches.base.commit is not None: + base_args.extend(("-p", push_branches.base.commit.commit_id)) + # We don't technically need to do this, but often tooling + # relies on pull requests being able to compute merge-base + # with the main branch. While the result you get here can be + # misleading (in particular, the merge-base will not + # 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( + "merge-base", base.commit_id, f"{self.remote_name}/{self.base}" + ) + if push_branches.base.commit is None or not self.sh.git( + "merge-base", + "--is-ancestor", + extra_base, + push_branches.base.commit.commit_id, + exitcode=True, + ): + base_args.extend(("-p", extra_base)) + new_base = GitCommitHash( + self.sh.git( + "commit-tree", + *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + *base_args, + base.tree, + input="{} (base update)\n\n[ghstack-poisoned]".format(self.msg), + ) + ) + head_args.extend(("-p", new_base)) + push_branches.base.update(GhCommit(new_base, base.tree)) + else: + # So, there is some complication here. We're computing what base + # to use based on the local situation on the user diff stack, but + # the remote merge structure may disagree with our local + # situation. For example, suppose I have a commit stack A - B, + # and then I insert a new commit A - M - B between them; + # previously B would have been based on A, but now it is based + # on M. What should happen here? + # + # Here are the high level correctness conditions: + # - No force pushes (history must be preserved) + # - GitHub displays a diff which is equivalent to the original + # user diff + # + # It turns out the logic here is fine, and the only thing it + # chokes on is rebasing back in time on master branch (you can't + # go back in time on PR branches, so this is a moot point there.) + # The problem is suppose you have: + # + # A - B - C + # \ \ + # M2 M1 # M1 was cherry-picked onto A, becoming M2 + # + # In branch form, this becomes: + # + # A - B - C + # \ \ + # \ M1 - M2 + # \ / + # \-----/ + # + # However, the merge base for C and M2 will always be computed to + # be B, because B is an ancestor of both C and M2, and it always + # beets out A (which is an ancestor of B). This means that you + # will diff M2 against B, which will typically result in "remove + # changes from B" spuriously showing up on the PR. + # + # When heads are always monotonically moving forward in time, + # there is not any problem with progressively more complicated + # merge histories, because we always specify the "correct" base + # branch. For example, consider: + # + # A - B + # \ + # \- X - Y1 + # \ + # \- Y2 + # + # Where Y1 is cherry-picked off of X onto B directly. In branch + # form, this becomes: + # + # A - B + # \ + # \- X - Y1 - Y2 + # + # But we update the base branch to be B, so we correctly diff Y2 + # against B (where here, the tree for Y2 no longer incorporates + # the changes for X). + # + # What does NOT work in this situation is if you manually (outside + # of ghstack) retarget Y2 back at X; we will spuriously report + # that the diff X and Y2 removes the changes from X. If you use + # ghstack, however, we will do this: + # + # A - B + # \ + # \- X - Y1 - Y2 - Y3 + # + # Where here Y3 has restored the changes from X, so the diff from + # X to Y3 checks out. + # + # It turns out there are a subset of manipulations, for which it + # is always safe to change the target base commit from GitHub UI + # without pushing a new commit. Intuitively, the idea is that + # once you add a commit as a merge base, you can't take it back: + # we always consider that branch to have been "merged in". So + # you can effectively only ever insert new commits between + # pre-existing commits, but once a commit depends on another + # commit, that dependency must always exist. I'm still + # considering whether or not we should force push by default in + # this sort of situation. + # + # By the way, what happens if you reorder commits? You get this + # funny looking graph: + # + # A - B + # \ + # X - Y - Y2 + # \ \ + # \------- X2 + + # We never have to create a base commit, we read it out from + # the base + if base_diff_meta is not None: + # The base was submitted the normal way (merge base is either + # next or head) + # + # We can always use next, because if head is OK, head will have + # been advanced to next anyway + # + # TODO: I do not feel this can be None + if base_diff_meta.head is not None: + # TODO: This assert is sus, next may be ahed of head + assert base_diff_meta.next == base_diff_meta.head + new_base = base_diff_meta.next + + if base_diff_meta.next == base_diff_meta.head: + # use head + base_branch = branch_head( + base_diff_meta.username, base_diff_meta.ghnum + ) + else: + # use next + base_branch = branch_next( + base_diff_meta.username, base_diff_meta.ghnum + ) + else: + # TODO: test that there isn't a more recent ancestor + # such that this doesn't actually work + new_base = base.commit_id + + base_branch = GitCommitHash(self.base) + + # 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( + "merge-base", + "--is-ancestor", + new_base, + push_branches.next.commit.commit_id, + exitcode=True, + ): + new_base = None + + if new_base is not None: + updated_base = True + head_args.extend(("-p", new_base)) + + # Check head commit if necessary + if ( + push_branches.head.commit is None + or updated_base + or push_branches.head.commit.tree != diff.tree + ): + new_head = GitCommitHash( + self.sh.git( + "commit-tree", + *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + *head_args, + diff.tree, + input="{}\n\n[ghstack-poisoned]".format(self.msg), + ) + ) + if self.direct: + # only update head branch if we're actually submitting + if submit: + push_branches.head.update(GhCommit(new_head, diff.tree)) + push_branches.next.update(GhCommit(new_head, diff.tree)) + else: + push_branches.head.update(GhCommit(new_head, diff.tree)) + + return push_branches, base_branch + + def _create_pull_request( + self, + diff: ghstack.diff.Diff, + base_diff_meta: Optional[DiffMeta], + ghnum: GhNumber, + ) -> DiffWithGitHubMetadata: + title, body = self._default_title_and_body(diff, None) + head_ref = branch_head(self.username, ghnum) + + if self.direct: + if base_diff_meta is None: + base_ref = self.base + else: + base_ref = branch_head(base_diff_meta.username, base_diff_meta.ghnum) + else: + base_ref = branch_base(self.username, ghnum) + + # Time to open the PR + # NB: GraphQL API does not support opening PRs + r = self.github.post( + "repos/{owner}/{repo}/pulls".format( + owner=self.repo_owner, repo=self.repo_name + ), + title=title, + head=head_ref, + base=base_ref, + body=body, + maintainer_can_modify=True, + draft=self.draft, + ) + number = r["number"] + + comment_id = None + if self.direct: + rc = self.github.post( + f"repos/{self.repo_owner}/{self.repo_name}/issues/{number}/comments", + body=f"{self.stack_header}:\n* (to be filled)", + ) + comment_id = rc["id"] + + logging.info("Opened PR #{}".format(number)) + + pull_request_resolved = ghstack.diff.PullRequestResolved( + owner=self.repo_owner, + repo=self.repo_name, + number=number, + github_url=self.github_url, + ) + + return DiffWithGitHubMetadata( + diff=diff, + number=number, + username=self.username, + remote_source_id=diff.source_id, # in sync + comment_id=comment_id, + title=title, + body=body, + closed=False, + ghnum=ghnum, + pull_request_resolved=pull_request_resolved, + head_ref=head_ref, + base_ref=base_ref, + ) + + def push_updates( + self, diffs_to_submit: List[DiffMeta], *, 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] = [] + + 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 + elif b == "base": + q = base_push_branches + else: + q = push_branches + q.append(push_spec(diff, branch(s.username, s.ghnum, b))) + # Careful! Don't push master. + # 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) + + for s in reversed(diffs_to_submit): + # NB: GraphQL API does not support modifying PRs + assert not s.closed + logging.info( + "# Updating https://{github_url}/{owner}/{repo}/pull/{number}".format( + github_url=self.github_url, + owner=self.repo_owner, + repo=self.repo_name, + number=s.number, + ) + ) + # TODO: don't update this if it doesn't need updating + base_kwargs = {} + if self.direct: + base_kwargs["base"] = s.base + else: + assert s.base == s.elab_diff.base_ref + stack_desc = self._format_stack(diffs_to_submit, s.number) + 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( + stack_desc, + s.body, + ), + title=s.title, + **base_kwargs, + ) + + 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, + ) + + # Report what happened + def format_url(s: DiffMeta) -> str: + return "https://{github_url}/{owner}/{repo}/pull/{number}".format( + github_url=self.github_url, + owner=self.repo_owner, + repo=self.repo_name, + number=s.number, + ) + + if self.short: + # Guarantee that the FIRST PR URL is the top of the stack + print("\n".join(format_url(s) for s in reversed(diffs_to_submit))) + return + + print() + print("# Summary of changes (ghstack {})".format(ghstack.__version__)) + print() + if diffs_to_submit: + for s in reversed(diffs_to_submit): + url = format_url(s) + print(" - {} {}".format(s.what, url)) + + print() + if import_help: + top_of_stack = diffs_to_submit[0] + + print("Meta employees can import your changes by running ") + print("(on a Meta machine):") + print() + print(" ghimport -s {}".format(format_url(top_of_stack))) + print() + print("If you want to work on this diff stack on another machine:") + print() + print(" ghstack checkout {}".format(format_url(top_of_stack))) + print("") + else: + print( + "No pull requests updated; all commits in your diff stack were empty!" + ) + + if self.ignored_diffs: + print() + print("FYI: I ignored the following commits, because they had no changes:") + print() + noop_pr = False + for d, elab_diff in reversed(self.ignored_diffs): + if elab_diff is None: + print(" - {} {}".format(d.oid[:8], d.title)) + else: + noop_pr = True + print( + " - {} {} (was previously submitted as PR #{})".format( + d.oid[:8], d.title, elab_diff.number + ) + ) + if noop_pr: + print() + print( + "I did NOT close or update PRs previously associated with these commits." + ) + + def check_invariants_for_diff( + self, + # the user diff is what the user actual sent us + user_commit_id: GitCommitHash, + orig_commit_id: GitCommitHash, + pre_branch_state: Optional[PreBranchState], + ) -> None: + def is_git_commit_hash(h: str) -> bool: + return re.match(r"[a-f0-9]{40}", h) is not None + + def assert_eq(a: Any, b: Any) -> None: + assert a == b, f"{a} != {b}" + + assert is_git_commit_hash(user_commit_id) + assert is_git_commit_hash(orig_commit_id) + if pre_branch_state: + assert is_git_commit_hash(pre_branch_state.head_commit_id) + assert is_git_commit_hash(pre_branch_state.base_commit_id) + + # 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) + ) + 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) + ) + assert_eq(orig_commit.commit_id, orig_commit_id) + assert not orig_commit.boundary + assert orig_parent_commit.boundary + + user_diff = ghstack.git.convert_header(user_commit, self.github_url) + orig_diff = ghstack.git.convert_header(orig_commit, self.github_url) + + # 1. Used same PR if it exists + if (pr := user_diff.pull_request_resolved) is not None: + assert_eq(pr, orig_diff.pull_request_resolved) + + # 2. Must have a PR after running + assert orig_diff.pull_request_resolved is not None + + # 3. We didn't corrupt the diff + assert_eq(user_commit.tree, orig_commit.tree) + assert_eq(user_parent_commit.tree, orig_parent_commit.tree) + + # 4. Orig diff has correct metadata + m = RE_GHSTACK_SOURCE_ID.search(orig_commit.commit_msg) + assert m is not None + assert_eq(m.group(1), orig_commit.tree) + + elaborated_orig_diff = 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}") + ) + assert_eq(head_commit.tree, user_commit.tree) + + base_ref = elaborated_orig_diff.base_ref + + if not self.direct: + assert_eq(base_ref, branch_base(self.username, elaborated_orig_diff.ghnum)) + else: + # TODO: assert the base is the head of the next branch, or main + pass + + (base_commit,) = ghstack.git.split_header( + self.sh.git("rev-list", "--header", "-1", f"{self.remote_name}/{base_ref}") + ) + # TODO: tree equality may not hold for self.direct, figure out a + # related invariant + if not self.direct: + assert_eq(base_commit.tree, user_parent_commit.tree) + + # 6. Orig commit was correctly pushed + assert_eq( + orig_commit.commit_id, + GitCommitHash( + self.sh.git( + "rev-parse", + self.remote_name + + "/" + + branch_orig(self.username, elaborated_orig_diff.ghnum), + ) + ), + ) + + # 7. Branches are either unchanged, or parent (no force pushes) + # NB: head is always merged in as first parent + # NB: you could relax this into an ancestor check + if pre_branch_state: + assert pre_branch_state.head_commit_id in [ + head_commit.commit_id, + head_commit.parents[0], + ] + # The base branch can change if we changed base in direct mode + if not self.direct: + assert pre_branch_state.base_commit_id in [ + base_commit.commit_id, + *([base_commit.parents[0]] if base_commit.parents else []), + ] + else: + # Direct commit parent typically have base, as it will be the + # main branch + if not self.direct: + pass + # This is now set to the orig base + # assert not base_commit.parents + + # 8. Head branch is not malformed + assert self.sh.git( + "merge-base", + "--is-ancestor", + base_commit.commit_id, + head_commit.commit_id, + exitcode=True, + ) + + # 9. Head and base branches are correctly poisoned + assert "[ghstack-poisoned]" in head_commit.commit_msg + + # TODO: direct PR based on main are not poisoned base commit + if not self.direct: + assert "[ghstack-poisoned]" in base_commit.commit_msg + + # ~~~~~~~~~~~~~~~~~~~~~~~~ + # Small helpers + + # TODO: do the tree formatting minigame + # Main things: + # - need to express some tree structure + # - want "as complete" a tree as possible; this may involve + # poking around the xrefs to find out all the other PRs + # involved in the stack + def _format_stack(self, diffs_to_submit: List[DiffMeta], number: int) -> str: + rows = [] + # NB: top is top of stack, opposite of update order + for s in diffs_to_submit: + if s.number == number: + rows.append(f"* __->__ #{s.number}") + else: + rows.append(f"* #{s.number}") + return self.stack_header + ":\n" + "\n".join(rows) + "\n" + + def _default_title_and_body( + self, diff: ghstack.diff.Diff, old_pr_body: Optional[str] + ) -> Tuple[str, str]: + """ + Compute what the default title and body of a newly opened pull + request would be, given the existing commit message. + + If you pass in the old PR body, we also preserve "Differential + Revision" information in the PR body. We only overwrite PR + body if you explicitly ask for it with --update-fields, but + it's good not to lose Phabricator diff assignment, so we special + case this. + """ + title = diff.title + extra = "" + if old_pr_body is not None: + # Look for tags we should preserve, and keep them + m = RE_DIFF_REV.search(old_pr_body) + if m: + extra = ( + "\n\nDifferential Revision: " + "[{phabdiff}]" + "(https://our.internmc.facebook.com/intern/diff/{phabdiff})" + ).format(phabdiff=m.group(1)) + commit_body = "".join(diff.summary.splitlines(True)[1:]).lstrip() + # Don't store ghstack-source-id in the PR body; it will become + # stale quickly + commit_body = RE_GHSTACK_SOURCE_ID.sub("", commit_body) + # Comment ID is not necessary; source of truth is orig commit + commit_body = RE_GHSTACK_COMMENT_ID.sub("", commit_body) + # Don't store Pull request in the PR body; it's + # unnecessary + commit_body = ghstack.diff.re_pull_request_resolved_w_sp(self.github_url).sub( + "", commit_body + ) + if self.direct: + pr_body = f"{commit_body}{extra}" + else: + if starts_with_bullet(commit_body): + commit_body = f"----\n\n{commit_body}" + pr_body = "{}:\n* (to be filled)\n\n{}{}".format( + self.stack_header, commit_body, extra + ) + return title, pr_body + + def _git_push(self, branches: Sequence[str], force: bool = False) -> None: + assert branches, "empty branches would push master, probably bad!" + try: + self.sh.git( + "push", + self.remote_name, + "--no-verify", + *(["--force"] if force else []), + *branches, + ) + except RuntimeError as e: + remote_url = self.sh.git("remote", "get-url", "--push", self.remote_name) + if remote_url.startswith("https://"): + raise RuntimeError( + "[E001] git push failed, probably because it asked for password " + "(scroll up to see original error). " + "Change your git URL to use SSH instead of HTTPS to enable passwordless push. " + "See https://github.com/ezyang/ghstack/wiki/E001 for more details." + ) from e + raise + self.github.push_hook(branches) + + +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" + ) + try: + hooks_path = sh.git( + "config", "--default", default_hooks_path, "--get", "core.hooksPath" + ) + hook_file = os.path.join(hooks_path, "pre-ghstack") + except Exception as e: + logging.warning(f"Pre ghstack hook failed: {e}") + return + + 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) diff --git a/src/ghstack/ghstack/test_prelude.py b/src/ghstack/ghstack/test_prelude.py new file mode 100644 index 0000000..1801e5a --- /dev/null +++ b/src/ghstack/ghstack/test_prelude.py @@ -0,0 +1,400 @@ +import argparse +import atexit +import contextlib +import io +import os +import re +import shutil +import stat +import sys +import tempfile +from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union + +from expecttest import assert_expected_inline + +import ghstack.github +import ghstack.github_fake +import ghstack.github_utils +import ghstack.land +import ghstack.shell +import ghstack.submit +import ghstack.unlink +from ghstack.types import GitCommitHash + +__all__ = [ + "ghstack", + "init_test", + "commit", + "git", + "gh_submit", + "gh_land", + "gh_unlink", + "GitCommitHash", + "checkout", + "amend", + "commit", + "cherry_pick", + "dump_github", + "ok", + "is_direct", + "write_file_and_add", + "assert_expected_inline", + "assert_raises", + "assert_expected_raises_inline", + "assert_github_state", + "assert_eq", + "get_sh", + "get_upstream_sh", + "get_github", + "tick", + "captured_output", +] + +GH_KEEP_TMP = os.getenv("GH_KEEP_TMP") + + +@contextlib.contextmanager +def captured_output() -> Iterator[Tuple[io.StringIO, io.StringIO]]: + new_out, new_err = io.StringIO(), io.StringIO() + old_out, old_err = sys.stdout, sys.stderr + try: + sys.stdout, sys.stderr = new_out, new_err + yield sys.stdout, sys.stderr + finally: + sys.stdout, sys.stderr = old_out, old_err + + +def strip_trailing_whitespace(text: str) -> str: + return re.sub(r" +$", "", text, flags=re.MULTILINE) + + +def indent(text: str, prefix: str) -> str: + return "".join( + prefix + line if line.strip() else line for line in text.splitlines(True) + ) + + +def handle_remove_read_only(func: Callable[..., Any], path: str, exc_info: Any) -> None: + """ + Error handler for ``shutil.rmtree``. + + If the error is due to an access error (read only file), + it attempts to add write permission and then retries. + + If the error is for another reason, it re-raises the error. + + Usage : ``shutil.rmtree(path, onerror=onerror)`` + """ + + if not os.access(path, os.W_OK): + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + +class Context: + github: ghstack.github.GitHubEndpoint + upstream_sh: ghstack.shell.Shell + sh: ghstack.shell.Shell + direct: bool + + def __init__(self, direct: bool) -> None: + # Set up a "parent" repository with an empty initial commit that we'll operate on + upstream_dir = tempfile.mkdtemp() + self.upstream_sh = ghstack.shell.Shell(cwd=upstream_dir, testing=True) + self.github = ghstack.github_fake.FakeGitHubEndpoint(self.upstream_sh) + + local_dir = tempfile.mkdtemp() + self.sh = ghstack.shell.Shell(cwd=local_dir, testing=True) + self.sh.git("clone", upstream_dir, ".") + self.direct = direct + + def cleanup(self) -> None: + if GH_KEEP_TMP: + print("upstream_dir preserved at: {}".format(self.upstream_sh.cwd)) + print("local_dir preserved at: {}".format(self.sh.cwd)) + else: + shutil.rmtree( + self.upstream_sh.cwd, + onerror=handle_remove_read_only, + ) + shutil.rmtree( + self.sh.cwd, + onerror=handle_remove_read_only, + ) + + def check_global_github_invariants(self, direct: bool) -> None: + r = self.github.graphql( + """ + query { + repository(name: "pytorch", owner: "pytorch") { + pullRequests { + nodes { + baseRefName + headRefName + closed + } + } + } + } + """ + ) + # No refs may be reused for multiple open PRs + seen_refs = set() + for pr in r["data"]["repository"]["pullRequests"]["nodes"]: + if pr["closed"]: + continue + # In direct mode, only head refs may not be reused; + # base refs can be reused in octopus situations + if not direct: + assert pr["baseRefName"] not in seen_refs + seen_refs.add(pr["baseRefName"]) + assert pr["headRefName"] not in seen_refs + seen_refs.add(pr["headRefName"]) + + +CTX: Context = None # type: ignore + + +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) + atexit.register(CTX.cleanup) + return CTX + + +@contextlib.contextmanager +def scoped_test(direct: bool) -> Iterator[None]: + global CTX + assert CTX is None + try: + CTX = Context(direct) + yield + finally: + CTX.cleanup() + CTX = None + + +# NB: returns earliest first +def gh_submit( + msg: str = "Update", + update_fields: bool = False, + short: bool = False, + no_skip: bool = False, + base: Optional[str] = None, + revs: Sequence[str] = (), + stack: bool = True, +) -> List[ghstack.submit.DiffMeta]: + self = CTX + r = ghstack.submit.main( + msg=msg, + username="ezyang", + github=self.github, + sh=self.sh, + update_fields=update_fields, + stack_header="Stack", + repo_owner_opt="pytorch", + repo_name_opt="pytorch", + short=short, + direct_opt=self.direct, + no_skip=no_skip, + github_url="github.com", + remote_name="origin", + base_opt=base, + revs=revs, + stack=stack, + check_invariants=True, + ) + self.check_global_github_invariants(self.direct) + return r + + +def gh_land(pull_request: str) -> None: + self = CTX + return ghstack.land.main( + remote_name="origin", + pull_request=pull_request, + github=self.github, + sh=self.sh, + github_url="github.com", + ) + + +def gh_unlink() -> None: + self = CTX + ghstack.unlink.main( + github=self.github, + sh=self.sh, + repo_owner="pytorch", + repo_name="pytorch", + github_url="github.com", + remote_name="origin", + ) + + +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) + + +def commit(name: str, msg: Optional[str] = None) -> None: + self = CTX + write_file_and_add(f"{name}.txt", "A") + self.sh.git( + "commit", + "-m", + f"Commit {name}\n\nThis is commit {name}" if msg is None else msg, + ) + self.sh.test_tick() + + +def amend(name: str) -> None: + self = CTX + write_file_and_add(f"{name}.txt", "A") + self.sh.git("commit", "--amend", "--no-edit", tick=True) + + +def git(*args: Any, **kwargs: Any) -> Any: + return CTX.sh.git(*args, **kwargs) + + +def ok() -> None: + print("\033[92m" + "TEST PASSED" + "\033[0m") + + +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) + + +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) + + +def dump_github() -> str: + self = CTX + r = self.github.graphql( + """ + query { + repository(name: "pytorch", owner: "pytorch") { + pullRequests { + nodes { + number + baseRefName + headRefName + title + body + closed + } + } + } + } + """ + ) + prs = [] + for pr in r["data"]["repository"]["pullRequests"]["nodes"]: + pr["body"] = indent(pr["body"].replace("\r", ""), " ") + # TODO: Use of git --graph here is a bit of a loaded + # footgun, because git doesn't really give any guarantees + # about what the graph should look like. So there isn't + # really any assurance that this will output the same thing + # on multiple test runs. We'll have to reimplement this + # ourselves to do it right. + # + # UPDATE: Another good reason to rewrite this is because git + # 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( + "log", + "--graph", + "--oneline", + "--pretty=format:%h %s", + f'{pr["baseRefName"]}..{pr["headRefName"]}', + ) + pr["commits"] = indent(strip_trailing_whitespace(pr["commits"]), " ") + else: + pr["commits"] = " (omitted)" + pr["status"] = "[X]" if pr["closed"] else "[O]" + prs.append( + "{status} #{number} {title} ({headRefName} -> {baseRefName})\n\n" + "{body}\n\n{commits}\n\n".format(**pr) + ) + + refs = self.upstream_sh.git( + "log", + "--graph", + "--oneline", + "--branches=gh/*/*/next", + "--branches=gh/*/*/head", + "--pretty=format:%h%d%n%w(0,3,3)%s", + ) + prs.append( + "Repository state:\n\n" + indent(strip_trailing_whitespace(refs), " ") + "\n" + ) + 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) + + +def is_direct() -> bool: + return CTX.direct + + +def assert_eq(a: Any, b: Any) -> None: + assert a == b, f"{a} != {b}" + + +def assert_raises( + exc_type: any, callable: Callable[..., any], *args: any, **kwargs: any +): + try: + callable(*args, **kwargs) + except exc_type: + return + assert False, "did not raise when expected to" + + +def assert_expected_raises_inline( + exc_type: any, callable: Callable[..., any], expect: str, *args: any, **kwargs: any +): + try: + callable(*args, **kwargs) + except exc_type as e: + assert_expected_inline(str(e), expect, skip=1) + return + assert False, "did not raise when expected to" + + +def get_sh() -> ghstack.shell.Shell: + return CTX.sh + + +def get_upstream_sh() -> ghstack.shell.Shell: + return CTX.upstream_sh + + +def get_github() -> ghstack.github.GitHubEndpoint: + return CTX.github + + +def tick() -> None: + CTX.sh.test_tick() diff --git a/src/ghstack/ghstack/trailers.py b/src/ghstack/ghstack/trailers.py new file mode 100644 index 0000000..0b788b7 --- /dev/null +++ b/src/ghstack/ghstack/trailers.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 + +import re +from typing import List, Tuple + +# Compile regexes once at module level for better performance +TRAILER_RE = re.compile(r"^([A-Za-z0-9_-]+)(\s*:\s*)(.*)$") +CONTINUATION_RE = re.compile(r"^\s+\S.*$") + +# Git-generated trailer prefixes +GIT_GENERATED_PREFIXES = ["Signed-off-by: ", "(cherry picked from commit "] + + +def parse_message(message: str) -> Tuple[str, str, str]: + """ + Parse a Git commit message into subject, body, and trailers. + + According to the Git documentation, trailers are: + - A group of one or more lines that is all trailers, or contains at least one + Git-generated or user-configured trailer and consists of at least 25% trailers. + - The group must be preceded by one or more empty (or whitespace-only) lines. + - The group must either be at the end of the message or be the last non-whitespace + lines before a line that starts with "---" (the "divider"). + + Args: + message: The commit message to parse. + + Returns: + A tuple containing: + - subject: The first line of the message + - body: The body of the message (may be empty) + - trailers: The trailer block as a raw string (may be empty) + """ + if not message: + return "", "", "" + + # Split into lines and get the subject (first line) + lines = message.splitlines() + subject = lines[0] if lines else "" + + if len(lines) <= 1: + return subject, "", "" + + # Remove subject + message_lines = lines[1:] + + if not message_lines: + return subject, "", "" + + # Find where the trailer block starts + trailer_start = find_trailer_block_start(message_lines) + + if trailer_start == -1: + # No trailer block found, everything after subject is body + body = "\n".join(message_lines).strip() + return subject, body, "" + + # Body is everything between subject and trailers (with empty lines trimmed) + body = "\n".join(message_lines[:trailer_start]).strip() + + # Keep trailers as a raw string + trailers = "\n".join(message_lines[trailer_start:]).strip() + + return subject, body, trailers + + +def find_trailer_block_start(lines: List[str]) -> int: + """ + Find the start index of the trailer block in a list of lines. + + Args: + lines: List of message lines (without subject and divider). + + Returns: + Index of the first line of the trailer block, or -1 if no trailer block is found. + """ + # Remove trailing empty lines + trimmed_lines = list(reversed([line for line in reversed(lines) if line.strip()])) + + if not trimmed_lines: + return -1 + + # Find the last non-empty block + block_indices = [-1] + [i for i, line in enumerate(lines) if not line.strip()] + + # Try blocks from last to first + for i in range(len(block_indices) - 1, -1, -1): + start_idx = block_indices[i] + 1 + # If we're at the beginning or checking the whole message + if i == 0 or start_idx == 0: + # Check if the whole remaining content is a trailer block + if is_trailer_block(lines[start_idx:]): + return start_idx + # No more blocks to check + return -1 + + # Check if the block after this blank line is a trailer block + end_idx = block_indices[i + 1] if i + 1 < len(block_indices) else len(lines) + if is_trailer_block(lines[start_idx:end_idx]): + return start_idx + + return -1 + + +def is_trailer_block(lines: List[str]) -> bool: + """ + Determine if the given lines form a trailer block. + + A block is a trailer block if: + 1. All lines are trailers, or + 2. At least one Git-generated trailer exists and at least 25% of lines are trailers + + Args: + lines: List of lines to check. + + Returns: + True if the lines form a trailer block, False otherwise. + """ + # Filter out empty lines + content_lines = [line for line in lines if line.strip()] + + if not content_lines: + return False + + trailer_lines = 0 + non_trailer_lines = 0 + has_git_generated_trailer = False + + i = 0 + while i < len(content_lines): + line = content_lines[i] + + # Skip continuation lines (they belong to the previous trailer) + if CONTINUATION_RE.match(line): + i += 1 + continue + + # Check if it's a git-generated trailer + if any(line.startswith(prefix) for prefix in GIT_GENERATED_PREFIXES): + has_git_generated_trailer = True + trailer_lines += 1 + elif TRAILER_RE.match(line): + # Regular trailer + trailer_lines += 1 + else: + # Not a trailer line + non_trailer_lines += 1 + + i += 1 + + # A block is a trailer block if all lines are trailers OR + # it has at least one git-generated trailer and >= 25% of lines are trailers + return (trailer_lines > 0 and non_trailer_lines == 0) or ( + has_git_generated_trailer and trailer_lines * 3 >= non_trailer_lines + ) + + +def interpret_trailers(message: str, trailers_to_add: List[str]) -> str: + """ + Add trailers to a commit message, mimicking git interpret-trailers. + + Args: + message: The commit message to add trailers to + trailers_to_add: List of trailers to add in the format "Key: Value" + + Returns: + The commit message with trailers added + """ + subject, body, existing_trailers = parse_message(message) + + # Create a new list with all trailers (existing + new) + all_trailers = [] + if existing_trailers: + all_trailers.append(existing_trailers) + + all_trailers.extend(trailers_to_add) + + # Build the new message + new_message = subject + + if body: + new_message += "\n\n" + body + + if all_trailers: + if body or (not body and existing_trailers): + new_message += "\n" + if not existing_trailers: + new_message += "\n" + new_message += "\n" + "\n".join(all_trailers) + + return new_message diff --git a/src/ghstack/ghstack/types.py b/src/ghstack/ghstack/types.py new file mode 100644 index 0000000..640a253 --- /dev/null +++ b/src/ghstack/ghstack/types.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from typing import NewType + +# A bunch of commonly used type definitions. + +PhabricatorDiffNumberWithD = NewType( + "PhabricatorDiffNumberWithD", str +) # aka "D1234567" + +GitHubNumber = NewType("GitHubNumber", int) # aka 1234 (as in #1234) + +# GraphQL ID that identifies Repository from GitHubb schema; +# aka MDExOlB1bGxSZXF1ZXN0MjU2NDM3MjQw +GitHubRepositoryId = NewType("GitHubRepositoryId", str) + +# aka 12 (as in gh/ezyang/12/base) +GhNumber = NewType("GhNumber", str) + +# Actually, sometimes we smuggle revs in here. We shouldn't. +# We want to guarantee that they're full canonical revs so that +# you can do equality on them without fear. +# commit 3f72e04eeabcc7e77f127d3e7baf2f5ccdb148ee +GitCommitHash = NewType("GitCommitHash", str) + +# tree 3f72e04eeabcc7e77f127d3e7baf2f5ccdb148ee +GitTreeHash = NewType("GitTreeHash", str) diff --git a/src/ghstack/ghstack/unlink.py b/src/ghstack/ghstack/unlink.py new file mode 100644 index 0000000..ce790f3 --- /dev/null +++ b/src/ghstack/ghstack/unlink.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +import logging +import re +import textwrap +from typing import List, Optional, Set + +import ghstack.diff +import ghstack.git +import ghstack.github +import ghstack.github_utils +import ghstack.gpg_sign +import ghstack.shell +from ghstack.types import GitCommitHash + +RE_GHSTACK_SOURCE_ID = re.compile(r"^ghstack-source-id: (.+)\n?", re.MULTILINE) + + +def main( + *, + commits: Optional[List[str]] = None, + github: ghstack.github.GitHubEndpoint, + sh: Optional[ghstack.shell.Shell] = None, + repo_owner: Optional[str] = None, + repo_name: Optional[str] = None, + github_url: str, + remote_name: str, + repo_default_branch_opt: Optional[str] = None, +) -> GitCommitHash: + # If commits is empty, we unlink the entire stack + # + # For now, we only process commits on our current + # stack, because we have no way of knowing how to + # "restack" for other commits. + + if sh is None: + # Use CWD + sh = ghstack.shell.Shell() + + default_branch = repo_default_branch_opt + if default_branch is None: + default_branch = 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"] + + # 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))) + + base = GitCommitHash( + sh.git("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") + ) + + # sanity check the parsed_commits + if parsed_commits is not None: + stack_commits = set() + for s in stack: + stack_commits.add(s.commit_id) + invalid_commits = parsed_commits - stack_commits + if invalid_commits: + raise RuntimeError( + "unlink can only process commits which are on the " + "current stack; these commits are not:\n{}".format( + "\n".join(invalid_commits) + ) + ) + + # Run the interactive rebase. Don't start rewriting until we + # hit the first commit that needs it. + head = base + rewriting = False + + for s in stack: + commit_id = s.commit_id + should_unlink = parsed_commits is None or commit_id in parsed_commits + if not rewriting and not should_unlink: + # Advance HEAD without reconstructing commit + head = commit_id + continue + + rewriting = True + commit_msg = s.commit_msg + logging.debug("-- commit_msg:\n{}".format(textwrap.indent(commit_msg, " "))) + if should_unlink: + commit_msg = RE_GHSTACK_SOURCE_ID.sub( + "", + ghstack.diff.re_pull_request_resolved_w_sp(github_url).sub( + "", commit_msg + ), + ) + logging.debug( + "-- edited commit_msg:\n{}".format(textwrap.indent(commit_msg, " ")) + ) + head = GitCommitHash( + sh.git( + "commit-tree", + *ghstack.gpg_sign.gpg_args_if_necessary(sh), + s.tree, + "-p", + head, + input=commit_msg, + ) + ) + + sh.git("reset", "--soft", head) + + logging.info( + """ +Diffs successfully unlinked! + +To undo this operation, run: + + git reset --soft {} +""".format( + s.commit_id + ) + ) + + return head From 0720b4e50196b94f2530f3408aba076bd6da05b8 Mon Sep 17 00:00:00 2001 From: Kevin Button Date: Thu, 3 Jul 2025 22:22:15 +0000 Subject: [PATCH 2/2] move dirs --- src/ghstack/cli.py | 7 + src/ghstack/config.py | 35 + src/ghstack/ghstack/__init__.py | 10 - src/ghstack/ghstack/__main__.py | 6 - src/ghstack/ghstack/action.py | 44 - src/ghstack/ghstack/cache.py | 76 - src/ghstack/ghstack/checkout.py | 31 - src/ghstack/ghstack/circleci.py | 37 - src/ghstack/ghstack/circleci_real.py | 87 - src/ghstack/ghstack/cli.py | 297 - src/ghstack/ghstack/config.py | 258 - src/ghstack/ghstack/diff.py | 117 - src/ghstack/ghstack/forensics.py | 129 - src/ghstack/ghstack/git.py | 99 - src/ghstack/ghstack/github.py | 97 - src/ghstack/ghstack/github_fake.py | 499 -- src/ghstack/ghstack/github_real.py | 243 - src/ghstack/ghstack/github_schema.graphql | 9067 --------------------- src/ghstack/ghstack/github_utils.py | 141 - src/ghstack/ghstack/gpg_sign.py | 41 - src/ghstack/ghstack/land.py | 204 - src/ghstack/ghstack/logs.py | 182 - src/ghstack/ghstack/py.typed | 0 src/ghstack/ghstack/rage.py | 120 - src/ghstack/ghstack/shell.py | 339 - src/ghstack/ghstack/status.py | 146 - src/ghstack/ghstack/submit.py | 1849 ----- src/ghstack/ghstack/test_prelude.py | 400 - src/ghstack/ghstack/trailers.py | 191 - src/ghstack/ghstack/types.py | 27 - src/ghstack/ghstack/unlink.py | 133 - src/ghstack/land.py | 21 +- src/ghstack/submit.py | 42 +- src/ghstack/unlink.py | 19 +- 34 files changed, 96 insertions(+), 14898 deletions(-) delete mode 100644 src/ghstack/ghstack/__init__.py delete mode 100755 src/ghstack/ghstack/__main__.py delete mode 100644 src/ghstack/ghstack/action.py delete mode 100644 src/ghstack/ghstack/cache.py delete mode 100644 src/ghstack/ghstack/checkout.py delete mode 100644 src/ghstack/ghstack/circleci.py delete mode 100644 src/ghstack/ghstack/circleci_real.py delete mode 100644 src/ghstack/ghstack/cli.py delete mode 100644 src/ghstack/ghstack/config.py delete mode 100644 src/ghstack/ghstack/diff.py delete mode 100644 src/ghstack/ghstack/forensics.py delete mode 100644 src/ghstack/ghstack/git.py delete mode 100644 src/ghstack/ghstack/github.py delete mode 100644 src/ghstack/ghstack/github_fake.py delete mode 100644 src/ghstack/ghstack/github_real.py delete mode 100644 src/ghstack/ghstack/github_schema.graphql delete mode 100644 src/ghstack/ghstack/github_utils.py delete mode 100644 src/ghstack/ghstack/gpg_sign.py delete mode 100644 src/ghstack/ghstack/land.py delete mode 100644 src/ghstack/ghstack/logs.py delete mode 100644 src/ghstack/ghstack/py.typed delete mode 100644 src/ghstack/ghstack/rage.py delete mode 100644 src/ghstack/ghstack/shell.py delete mode 100644 src/ghstack/ghstack/status.py delete mode 100644 src/ghstack/ghstack/submit.py delete mode 100644 src/ghstack/ghstack/test_prelude.py delete mode 100644 src/ghstack/ghstack/trailers.py delete mode 100644 src/ghstack/ghstack/types.py delete mode 100644 src/ghstack/ghstack/unlink.py diff --git a/src/ghstack/cli.py b/src/ghstack/cli.py index 7b79aed..b178aa1 100644 --- a/src/ghstack/cli.py +++ b/src/ghstack/cli.py @@ -141,6 +141,7 @@ def land(force: bool, pull_request: str) -> None: github_url=config.github_url, remote_name=config.remote_name, force=force, + repo_default_branch_opt=config.repo_default_branch, ) @@ -271,6 +272,11 @@ def submit( revs=revs, stack=stack, direct_opt=direct_opt, + repo_name_opt=config.repo_name, + repo_owner_opt=config.repo_owner, + repo_is_fork_opt=config.repo_is_fork, + repo_id_opt=config.repo_id, + repo_default_branch_opt=config.repo_default_branch, ) @@ -287,4 +293,5 @@ def unlink(commits: List[str]) -> None: sh=shell, github_url=config.github_url, remote_name=config.remote_name, + repo_default_branch_opt=config.repo_default_branch, ) diff --git a/src/ghstack/config.py b/src/ghstack/config.py index 72a91d0..e8ad17d 100644 --- a/src/ghstack/config.py +++ b/src/ghstack/config.py @@ -40,6 +40,16 @@ ("github_url", str), # Name of the upstream remote ("remote_name", str), + # Repository name + ("repo_name", Optional[str]), + # Repository owner / organisation + ("repo_owner", Optional[str]), + # Whether the repo is a fork + ("repo_is_fork", Optional[str]), + # Numeric GitHub repository ID + ("repo_id", Optional[str]), + # Default branch name (e.g. "main") + ("repo_default_branch", Optional[str]), ], ) @@ -208,6 +218,26 @@ def read_config( config.write(f) logging.info("NB: configuration saved to {}".format(config_path)) + repo_name = None + if config.has_option("repo", "name"): + repo_name = config.get("repo", "name") + + repo_owner = None + if config.has_option("repo", "owner"): + repo_owner = config.get("repo", "owner") + + repo_is_fork = None + if config.has_option("repo", "is_fork"): + repo_is_fork = config.getboolean("repo", "is_fork") + + repo_id = None + if config.has_option("repo", "id"): + repo_id = config.get("repo", "id") + + repo_default_branch = None + if config.has_option("repo", "default_branch"): + repo_default_branch = config.get("repo", "default_branch") + conf = Config( github_oauth=github_oauth, circle_token=circle_token, @@ -218,6 +248,11 @@ def read_config( default_project_dir=default_project_dir, github_url=github_url, remote_name=remote_name, + repo_name=repo_name, + repo_owner=repo_owner, + repo_is_fork=repo_is_fork, + repo_id=repo_id, + repo_default_branch=repo_default_branch, ) logging.debug(f"conf = {conf}") return conf diff --git a/src/ghstack/ghstack/__init__.py b/src/ghstack/ghstack/__init__.py deleted file mode 100644 index bb565c9..0000000 --- a/src/ghstack/ghstack/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -if sys.version_info >= (3, 8): - import importlib.metadata as importlib_metadata -else: - import importlib_metadata - -__version__ = importlib_metadata.version("ghstack") # type: ignore[no-untyped-call] diff --git a/src/ghstack/ghstack/__main__.py b/src/ghstack/ghstack/__main__.py deleted file mode 100755 index 866b1f4..0000000 --- a/src/ghstack/ghstack/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -from ghstack.cli import main - -if __name__ == "__main__": - main() diff --git a/src/ghstack/ghstack/action.py b/src/ghstack/ghstack/action.py deleted file mode 100644 index ac6e1b6..0000000 --- a/src/ghstack/ghstack/action.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 - -import logging -from typing import Optional - -import ghstack.github -import ghstack.github_utils -import ghstack.shell - - -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( - """ - query ($owner: String!, $name: String!, $number: Int!) { - repository(name: $name, owner: $owner) { - pullRequest(number: $number) { - id - } - } - } - """, - **params, - ) - pr_id = pr_result["data"]["repository"]["pullRequest"]["id"] - - if close: - logging.info("Closing {owner}/{name}#{number}".format(**params)) - github.graphql( - """ - mutation ($input: ClosePullRequestInput!) { - closePullRequest(input: $input) { - clientMutationId - } - } - """, - input={"pullRequestId": pr_id, "clientMutationId": "A"}, - ) diff --git a/src/ghstack/ghstack/cache.py b/src/ghstack/ghstack/cache.py deleted file mode 100644 index c0eb71b..0000000 --- a/src/ghstack/ghstack/cache.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 - -# A simple, key-value cache. -# - Concurrency safe -# - Handles eviction - -import os -import sqlite3 -from typing import Optional - -_handle: Optional[sqlite3.Connection] = None -CACHE_SIZE = 1000 -CURRENT_VERSION = 1 - - -def _db_conn() -> sqlite3.Connection: - global _handle - fn = os.path.expanduser("~/.ghstackcache") - if not _handle: - _handle = sqlite3.connect(fn) - user_version = _handle.execute("PRAGMA user_version").fetchone() - if user_version is None or user_version[0] != CURRENT_VERSION: - _handle.close() - os.remove(fn) - _handle = sqlite3.connect(fn) - _handle.execute( - """ - CREATE TABLE ghstack_cache ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - domain TEXT, - key TEXT, - value TEXT - ) - """ - ) - _handle.execute( - """ - CREATE UNIQUE INDEX domain_key ON ghstack_cache (domain, key) - """ - ) - _handle.execute("PRAGMA user_version = {}".format(CURRENT_VERSION)) - _handle.commit() - return _handle - - -def get(domain: str, key: str) -> Optional[str]: - conn = _db_conn() - c = conn.execute( - "SELECT value FROM ghstack_cache WHERE domain = ? AND key = ?", (domain, key) - ) - r = c.fetchone() - if r is None: - return None - r = r[0] - assert isinstance(r, str) - return r - - -def put(domain: str, key: str, value: str) -> None: - conn = _db_conn() - conn.execute( - "UPDATE ghstack_cache SET value = ? WHERE domain = ? AND key = ?", - (value, domain, key), - ) - c = conn.execute( - """ - INSERT INTO ghstack_cache (domain, key, value) - SELECT ?, ?, ? WHERE (SELECT Changes() = 0) - """, - (domain, key, value), - ) - if c.lastrowid is not None: - conn.execute( - "DELETE FROM ghstack_cache WHERE id < ?", (c.lastrowid - CACHE_SIZE,) - ) - conn.commit() diff --git a/src/ghstack/ghstack/checkout.py b/src/ghstack/ghstack/checkout.py deleted file mode 100644 index 4767297..0000000 --- a/src/ghstack/ghstack/checkout.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import re - -import ghstack.github -import ghstack.github_utils -import ghstack.shell - - -def main( - pull_request: str, - github: ghstack.github.GitHubEndpoint, - sh: ghstack.shell.Shell, - remote_name: str, -) -> None: - - params = ghstack.github_utils.parse_pull_request( - pull_request, sh=sh, remote_name=remote_name - ) - head_ref = github.get_head_ref(**params) - orig_ref = re.sub(r"/head$", "/orig", head_ref) - if orig_ref == head_ref: - logging.warning( - "The ref {} doesn't look like a ghstack reference".format(head_ref) - ) - - # TODO: Handle remotes correctly too (so this subsumes hub) - - sh.git("fetch", "--prune", remote_name) - sh.git("checkout", remote_name + "/" + orig_ref) diff --git a/src/ghstack/ghstack/circleci.py b/src/ghstack/ghstack/circleci.py deleted file mode 100644 index 18cad79..0000000 --- a/src/ghstack/ghstack/circleci.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 - -from abc import ABCMeta, abstractmethod -from typing import Any - - -class CircleCIEndpoint(metaclass=ABCMeta): - async def get(self, path: str, **kwargs: Any) -> Any: - """ - Send a GET request to endpoint 'path'. - - Returns: parsed JSON response - """ - return await self.rest("get", path, **kwargs) - - async def post(self, path: str, **kwargs: Any) -> Any: - """ - Send a POST request to endpoint 'path'. - - Returns: parsed JSON response - """ - return await self.rest("post", path, **kwargs) - - @abstractmethod - async 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, - does NOT include the API version number - **kwargs: dictionary of JSON payload to send - - Returns: parsed JSON response - """ - pass diff --git a/src/ghstack/ghstack/circleci_real.py b/src/ghstack/ghstack/circleci_real.py deleted file mode 100644 index 16cf09e..0000000 --- a/src/ghstack/ghstack/circleci_real.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 - -import json -import logging -import re -from typing import Any, Optional - -import aiohttp - -import ghstack.cache -import ghstack.circleci - -RE_BUILD_PATH = re.compile(r"^project/github/[^/]+/[^/]+/[0-9]+$") - - -class RealCircleCIEndpoint(ghstack.circleci.CircleCIEndpoint): - rest_endpoint: str = "https://circleci.com/api/v1.1" - - # The API token to authenticate to CircleCI with - # https://circleci.com/account/api - circle_token: Optional[str] - - # The URL of a proxy to use for these connections (for - # Facebook users, this is typically 'http://fwdproxy:8080') - proxy: Optional[str] - - def __init__( - self, *, circle_token: Optional[str] = None, proxy: Optional[str] = None - ): - self.circle_token = circle_token - self.proxy = proxy - - async def rest(self, method: str, path: str, **kwargs: Any) -> Any: - headers = { - "Content-Type": "application/json", - "User-Agent": "ghstack", - } - - url = self.rest_endpoint + "/" + path - logging.debug("# {} {}".format(method, url)) - logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1))) - - params = {} - if self.circle_token: - params["circle-token"] = self.circle_token - - is_get_build = method == "get" and RE_BUILD_PATH.match(path) - - if is_get_build: - # consult cache - cache_result = ghstack.cache.get("circleci", path) - if cache_result is not None: - logging.debug("Retrieved result from cache") - return json.loads(cache_result) - - async with aiohttp.request( - method.upper(), - url, - params=params, - json=kwargs, - headers=headers, - proxy=self.proxy, - ) as resp: - logging.debug("Response status: {}".format(resp.status)) - - r_text = await resp.text() - - try: - r = json.loads(r_text) - except json.decoder.JSONDecodeError: - logging.debug("Response body:\n{}".format(r_text)) - raise - else: - pretty_json = json.dumps(r, indent=1) - logging.debug("Response JSON:\n{}".format(pretty_json)) - - try: - resp.raise_for_status() - except aiohttp.ClientResponseError: - raise RuntimeError(pretty_json) - - # NB: Don't save to cache if it's still running - if is_get_build and r["outcome"] is not None: - logging.debug("Saving result to cache") - ghstack.cache.put("circleci", path, r_text) - - return r diff --git a/src/ghstack/ghstack/cli.py b/src/ghstack/ghstack/cli.py deleted file mode 100644 index b178aa1..0000000 --- a/src/ghstack/ghstack/cli.py +++ /dev/null @@ -1,297 +0,0 @@ -import asyncio -import contextlib -from typing import Generator, List, Optional, Tuple - -import click - -import ghstack -import ghstack.action -import ghstack.checkout -import ghstack.circleci_real -import ghstack.config -import ghstack.github_real -import ghstack.land -import ghstack.logs -import ghstack.rage -import ghstack.status -import ghstack.submit -import ghstack.unlink - -EXIT_STACK = contextlib.ExitStack() - -GhstackContext = Tuple[ - ghstack.shell.Shell, - ghstack.config.Config, - ghstack.github_real.RealGitHubEndpoint, -] - - -@contextlib.contextmanager -def cli_context( - *, - request_circle_token: bool = False, - request_github_token: bool = True, -) -> Generator[GhstackContext, None, None]: - with EXIT_STACK: - shell = ghstack.shell.Shell() - config = ghstack.config.read_config( - request_circle_token=request_circle_token, - request_github_token=request_github_token, - ) - github = ghstack.github_real.RealGitHubEndpoint( - oauth_token=config.github_oauth, - proxy=config.proxy, - github_url=config.github_url, - ) - yield shell, config, github - - -@click.group(invoke_without_command=True) -@click.pass_context -@click.version_option(ghstack.__version__, "--version", "-V") -@click.option("--debug", is_flag=True, help="Log debug information to stderr") -# hidden arguments that we'll pass along to submit if no other command given -@click.option("--message", "-m", default="Update", hidden=True) -@click.option("--update-fields", "-u", is_flag=True, hidden=True) -@click.option("--short", is_flag=True, hidden=True) -@click.option("--force", is_flag=True, hidden=True) -@click.option("--no-skip", is_flag=True, hidden=True) -@click.option("--draft", is_flag=True, hidden=True) -@click.option( - "--direct/--no-direct", "direct_opt", is_flag=True, hidden=True, default=None -) -@click.option("--base", "-B", default=None, hidden=True) -@click.option("--stack/--no-stack", "-s/-S", is_flag=True, default=True, hidden=True) -def main( - ctx: click.Context, - debug: bool, - message: str, - update_fields: bool, - short: bool, - force: bool, - direct_opt: Optional[bool], - no_skip: bool, - draft: bool, - base: Optional[str], - stack: bool, -) -> None: - """ - Submit stacks of diffs to Github - """ - EXIT_STACK.enter_context(ghstack.logs.manager(debug=debug)) - - if not ctx.invoked_subcommand: - ctx.invoke( - submit, - message=message, - update_fields=update_fields, - short=short, - force=force, - no_skip=no_skip, - draft=draft, - base=base, - stack=stack, - direct_opt=direct_opt, - ) - - -@main.command("action") -@click.option("--close", is_flag=True, help="Close the specified pull request") -@click.argument("pull_request", metavar="PR") -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, - ) - - -@main.command("checkout") -@click.argument("pull_request", metavar="PR") -def checkout(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, - ) - - -@main.command("land") -@click.option("--force", is_flag=True, help="force land even if the PR is closed") -@click.argument("pull_request", metavar="PR") -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, - repo_default_branch_opt=config.repo_default_branch, - ) - - -@main.command("rage") -@click.option( - "--latest", - is_flag=True, - help="Select the last command (not including rage commands) to report", -) -def rage(latest: bool) -> None: - with cli_context(request_github_token=False): - ghstack.rage.main(latest) - - -@main.command("status") -@click.argument("pull_request", metavar="PR") -def status(pull_request: str) -> None: - """ - Check status of a PR - """ - with cli_context(request_circle_token=True) as (shell, config, github): - circleci = ghstack.circleci_real.RealCircleCIEndpoint( - circle_token=config.circle_token - ) - - fut = 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") -@click.option( - "--message", - "-m", - default="Update", - help="Description of change you made", -) -@click.option( - "--update-fields", - "-u", - is_flag=True, - help="Update GitHub pull request summary from the local commit", -) -@click.option( - "--short", is_flag=True, help="Print only the URL of the latest opened PR to stdout" -) -@click.option( - "--force", - is_flag=True, - help="force push the branch even if your local branch is stale", -) -@click.option( - "--no-skip", - is_flag=True, - help="Never skip pushing commits, even if the contents didn't change " - "(use this if you've only updated the commit message).", -) -@click.option( - "--draft", - is_flag=True, - help="Create the pull request in draft mode (only if it has not already been created)", -) -@click.option( - "--base", - "-B", - default=None, - help="Branch to base the stack off of; " - "defaults to the default branch of a repository", -) -@click.option( - "--stack/--no-stack", - "-s/-S", - is_flag=True, - default=True, - help="Submit the entire of stack of commits reachable from HEAD, versus only single commits. " - "This affects the meaning of REVS. With --stack, we submit all commits that " - "are reachable from REVS, excluding commits already on the base branch. Revision ranges " - "supported by git rev-list are also supported. " - "With --no-stack, we support only non-range identifiers, and will submit each commit " - "listed in the command line.", -) -@click.option( - "--direct/--no-direct", - "direct_opt", - default=None, - is_flag=True, - help="Create stack that directly merges into master", -) -@click.argument( - "revs", - nargs=-1, - metavar="REVS", -) -def submit( - message: str, - update_fields: bool, - short: bool, - force: bool, - no_skip: bool, - draft: bool, - direct_opt: Optional[bool], - base: Optional[str], - revs: Tuple[str, ...], - stack: bool, -) -> None: - """ - 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, - repo_name_opt=config.repo_name, - repo_owner_opt=config.repo_owner, - repo_is_fork_opt=config.repo_is_fork, - repo_id_opt=config.repo_id, - repo_default_branch_opt=config.repo_default_branch, - ) - - -@main.command("unlink") -@click.argument("commits", nargs=-1, metavar="COMMIT") -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, - repo_default_branch_opt=config.repo_default_branch, - ) diff --git a/src/ghstack/ghstack/config.py b/src/ghstack/ghstack/config.py deleted file mode 100644 index e8ad17d..0000000 --- a/src/ghstack/ghstack/config.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python3 - -import configparser -import getpass -import logging -import os -import re -from pathlib import Path -from typing import NamedTuple, Optional - -import requests - -import ghstack.logs - -DEFAULT_GHSTACKRC_PATH = Path.home() / ".ghstackrc" -GHSTACKRC_PATH_VAR = "GHSTACKRC_PATH" - -Config = NamedTuple( - "Config", - [ - # Proxy to use when making connections to GitHub - ("proxy", Optional[str]), - # OAuth token to authenticate to GitHub with - ("github_oauth", Optional[str]), - # GitHub username; used to namespace branches we create - ("github_username", str), - # Token to authenticate to CircleCI with - ("circle_token", Optional[str]), - # These config parameters are not used by ghstack, but other - # tools that reuse this module - # Path to working fbsource checkout - ("fbsource_path", str), - # Path to working git checkout (ghstack infers your git checkout - # based on CWD) - ("github_path", str), - # Path to project directory inside fbsource, to default when - # autodetection fails - ("default_project_dir", str), - # GitHub url. Defaults to github.com which is true for all non-enterprise github repos - ("github_url", str), - # Name of the upstream remote - ("remote_name", str), - # Repository name - ("repo_name", Optional[str]), - # Repository owner / organisation - ("repo_owner", Optional[str]), - # Whether the repo is a fork - ("repo_is_fork", Optional[str]), - # Numeric GitHub repository ID - ("repo_id", Optional[str]), - # Default branch name (e.g. "main") - ("repo_default_branch", Optional[str]), - ], -) - - -def get_path_from_env_var(var_name: str) -> Optional[Path]: - if (path := os.environ.get(var_name)) is not None: - return Path(path).expanduser().resolve() - return None - - -def read_config( - *, - request_circle_token: bool = False, - request_github_token: bool = True, -) -> Config: # noqa: C901 - config = configparser.ConfigParser() - - config_path = None - current_dir = Path(os.getcwd()) - - while current_dir != current_dir.parent: - tentative_config_path = "/".join([str(current_dir), ".ghstackrc"]) - if os.path.exists(tentative_config_path): - config_path = tentative_config_path - break - current_dir = current_dir.parent - - write_back = False - if config_path is None: - config_path = str( - get_path_from_env_var(GHSTACKRC_PATH_VAR) or DEFAULT_GHSTACKRC_PATH - ) - write_back = True - - logging.debug(f"config_path = {config_path}") - config.read([".ghstackrc", config_path]) - - if not config.has_section("ghstack"): - config.add_section("ghstack") - write_back = True - - if config.has_option("ghstack", "github_url"): - github_url = config.get("ghstack", "github_url") - else: - github_url = input("GitHub enterprise domain (leave blank for OSS GitHub): ") - if not github_url: - github_url = "github.com" - if not re.match(r"[\w\.-]+\.\w+$", github_url): - raise RuntimeError( - f"{github_url} is not a valid domain name (do not include http:// scheme)" - ) - config.set("ghstack", "github_url", github_url) - write_back = True - - # Environment variable overrides config file - # This envvar is legacy from ghexport days - github_oauth = os.getenv("OAUTH_TOKEN") - if github_oauth is not None: - logging.warning( - "Deprecated OAUTH_TOKEN environment variable used to populate github_oauth--" - "this is probably not what you intended; unset OAUTH_TOKEN from your " - "environment to use the setting in .ghstackrc instead." - ) - if github_oauth is None and config.has_option("ghstack", "github_oauth"): - github_oauth = config.get("ghstack", "github_oauth") - if github_oauth is None and request_github_token: - print("Generating GitHub access token...") - CLIENT_ID = "89cc88ca50efbe86907a" - res = requests.post( - f"https://{github_url}/login/device/code", - headers={"Accept": "application/json"}, - data={"client_id": CLIENT_ID, "scope": "repo"}, - ) - data = res.json() - print(f"User verification code: {data['user_code']}") - print("Go to https://github.com/login/device and enter the code.") - print("Once you've authorized ghstack, press any key to continue...") - input() - - res = requests.post( - f"https://{github_url}/login/oauth/access_token", - headers={"Accept": "application/json"}, - data={ - "client_id": CLIENT_ID, - "device_code": data["device_code"], - "grant_type": "urn:ietf:params:oauth:grant-type:device_code", - }, - ) - github_oauth = res.json()["access_token"] - config.set("ghstack", "github_oauth", github_oauth) - write_back = True - if github_oauth is not None: - ghstack.logs.formatter.redact(github_oauth, "") - - circle_token = None - if circle_token is None and config.has_option("ghstack", "circle_token"): - circle_token = config.get("ghstack", "circle_token") - if circle_token is None and request_circle_token: - circle_token = getpass.getpass( - "CircleCI Personal API token (make one at " - "https://circleci.com/account/api ): " - ).strip() - config.set("ghstack", "circle_token", circle_token) - write_back = True - if circle_token is not None: - ghstack.logs.formatter.redact(circle_token, "") - - github_username = None - if config.has_option("ghstack", "github_username"): - github_username = config.get("ghstack", "github_username") - if github_username is None and github_oauth is not None: - request_url: str - if github_url == "github.com": - request_url = f"https://api.{github_url}/user" - else: - request_url = f"https://{github_url}/api/v3/user" - res = requests.get( - request_url, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": f"Bearer {github_oauth}", - "X-GitHub-Api-Version": "2022-11-28", - }, - ) - res.raise_for_status() - github_username = res.json()["login"] - config.set("ghstack", "github_username", github_username) - write_back = True - if github_username is None: - github_username = input("GitHub username: ") - if not re.match( - r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", github_username, re.I - ): - raise RuntimeError( - "{} is not a valid GitHub username".format(github_username) - ) - config.set("ghstack", "github_username", github_username) - write_back = True - - proxy = None - if config.has_option("ghstack", "proxy"): - proxy = config.get("ghstack", "proxy") - - if config.has_option("ghstack", "fbsource_path"): - fbsource_path = config.get("ghstack", "fbsource_path") - else: - fbsource_path = os.path.expanduser("~/local/fbsource") - - if config.has_option("ghstack", "github_path"): - github_path = config.get("ghstack", "github_path") - else: - github_path = os.path.expanduser("~/local/ghstack-pytorch") - - if config.has_option("ghstack", "default_project"): - default_project_dir = config.get("ghstack", "default_project_dir") - else: - default_project_dir = "fbcode/caffe2" - - if config.has_option("ghstack", "remote_name"): - remote_name = config.get("ghstack", "remote_name") - else: - remote_name = "origin" - - if write_back: - with open(config_path, "w") as f: - config.write(f) - logging.info("NB: configuration saved to {}".format(config_path)) - - repo_name = None - if config.has_option("repo", "name"): - repo_name = config.get("repo", "name") - - repo_owner = None - if config.has_option("repo", "owner"): - repo_owner = config.get("repo", "owner") - - repo_is_fork = None - if config.has_option("repo", "is_fork"): - repo_is_fork = config.getboolean("repo", "is_fork") - - repo_id = None - if config.has_option("repo", "id"): - repo_id = config.get("repo", "id") - - repo_default_branch = None - if config.has_option("repo", "default_branch"): - repo_default_branch = config.get("repo", "default_branch") - - conf = Config( - github_oauth=github_oauth, - circle_token=circle_token, - github_username=github_username, - proxy=proxy, - fbsource_path=fbsource_path, - github_path=github_path, - default_project_dir=default_project_dir, - github_url=github_url, - remote_name=remote_name, - repo_name=repo_name, - repo_owner=repo_owner, - repo_is_fork=repo_is_fork, - repo_id=repo_id, - repo_default_branch=repo_default_branch, - ) - logging.debug(f"conf = {conf}") - return conf diff --git a/src/ghstack/ghstack/diff.py b/src/ghstack/ghstack/diff.py deleted file mode 100644 index 1fd5be7..0000000 --- a/src/ghstack/ghstack/diff.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -import re -from dataclasses import dataclass -from typing import Optional, Pattern - -from ghstack.types import GitHubNumber, GitTreeHash - -RE_GH_METADATA = re.compile( - r"gh-metadata: (?P[^/]+) (?P[^/]+) (?P[0-9]+) " - r"gh/(?P[a-zA-Z0-9-]+)/(?P[0-9]+)/head", - re.MULTILINE, -) - - -RAW_PULL_REQUEST_RESOLVED = ( - r"(Pull Request resolved|Pull-Request-resolved|Pull-Request): " - r"https://{github_url}/(?P[^/]+)/(?P[^/]+)/pull/(?P[0-9]+)" -) - - -def re_pull_request_resolved(github_url: str) -> Pattern[str]: - return re.compile(RAW_PULL_REQUEST_RESOLVED.format(github_url=github_url)) - - -def re_pull_request_resolved_w_sp(github_url: str) -> Pattern[str]: - return re.compile(r"\n*" + RAW_PULL_REQUEST_RESOLVED.format(github_url=github_url)) - - -@dataclass -class PullRequestResolved: - owner: str - repo: str - number: GitHubNumber - github_url: str - - def url(self) -> str: - return "https://{}/{}/{}/pull/{}".format( - self.github_url, self.owner, self.repo, self.number - ) - - @staticmethod - def search(s: str, github_url: str) -> Optional["PullRequestResolved"]: - m = re_pull_request_resolved(github_url).search(s) - if m is not None: - return PullRequestResolved( - owner=m.group("owner"), - repo=m.group("repo"), - number=GitHubNumber(int(m.group("number"))), - github_url=github_url, - ) - m = RE_GH_METADATA.search(s) - if m is not None: - return PullRequestResolved( - owner=m.group("owner"), - repo=m.group("repo"), - number=GitHubNumber(int(m.group("number"))), - github_url=github_url, - ) - return None - - -@dataclass -class Diff: - """ - An abstract representation of a diff. Typically represents git commits, - but we may also virtually be importing diffs from other VCSes, hence - the agnosticism. - """ - - # Title of the diff - title: str - - # Detailed description of the diff. Includes the title. - summary: str - - # Unique identifier representing the commit in question (may be a - # Git/Mercurial commit hash; the important thing is that it can be - # used as a unique identifier.) - oid: str - - # Unique identifier representing the commit in question, but it - # is *invariant* to changes in commit message / summary. In Git, - # a valid identifier would be the tree hash of the commit (rather - # than the commit hash itself); in Phabricator it could be the - # version of the diff. - # - # It is OK for this source id to wobble even if the tree stays the - # same. This simply means we will think there are changes even - # if there aren't any, which should be safe (but just generate - # annoying updates). What we would like is for the id to quiesce: - # if you didn't rebase your hg rev, the source id is guaranteed to - # be the same. - source_id: str - - # The contents of 'Pull-Request'. This is None for - # diffs that haven't been submitted by ghstack. For BC reasons, - # this also accepts gh-metadata. - pull_request_resolved: Optional[PullRequestResolved] - - # A git tree hash that represents the contents of this diff, if it - # were applied in Git. - # - # TODO: Constructing these tree hashes if they're not already in Git - # is a somewhat involved process, as you have to actually construct - # the git tree object (it's not guaranteed to exist already). I'm - # offloading this work onto the ghimport/ghexport tools. - tree: GitTreeHash - - # The name and email of the author, used so we can preserve - # authorship information when constructing a rebased commit - author_name: Optional[str] - author_email: Optional[str] - - # If this isn't actually a diff; it's a boundary commit (not part - # of the stack) that we've parsed for administrative purposes - boundary: bool diff --git a/src/ghstack/ghstack/forensics.py b/src/ghstack/ghstack/forensics.py deleted file mode 100644 index 479c7cd..0000000 --- a/src/ghstack/ghstack/forensics.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# This script looks at all commits downloads their logs and prints them -# for you - -import re -from typing import Any, Dict - -import aiohttp - -import ghstack.circleci -import ghstack.github -import ghstack.github_utils - -RE_CIRCLECI_URL = re.compile(r"^https://circleci.com/gh/pytorch/pytorch/([0-9]+)") - - -def strip_sccache(x: str) -> str: - sccache_marker = "=================== sccache compilation log ===================" - marker_pos = x.rfind(sccache_marker) - newline_before_marker_pos = x.rfind("\n", 0, marker_pos) - return x[:newline_before_marker_pos] - - -async def main( - pull_request: str, - github: ghstack.github.GitHubEndpoint, - circleci: ghstack.circleci.CircleCIEndpoint, -) -> None: - - # Game plan: - # 1. Query GitHub to find out what the current statuses are - # (TODO: if we got rate limited we'll miss stuff) - # 2. For each status in parallel: - # a. Query CircleCI for job status - # b. (Future work) Query output_url to get log information - # (it's gzip'ed) - # - # For now: - # - Print if the job actually ran, or was skipped - # - Easy way to determine: check if "Should run job after - # checkout" is last step - # - I inspected circleci.get('project/github/pytorch/pytorch/1773555') - # to see if there were other options, there did not appear - # 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) - - # TODO: stop hard-coding number of commits - r = github.graphql( - """ - query ($name: String!, $owner: String!, $number: Int!) { - repository(name: $name, owner: $owner) { - pullRequest(number: $number) { - commits(last: 100) { - nodes { - commit { - oid - messageHeadline - status { - contexts { - context - state - targetUrl - } - } - } - } - } - } - } - } - """, - **params, - ) - nodes = r["data"]["repository"]["pullRequest"]["commits"]["nodes"] - - async def process_node(n: Dict[str, Any]) -> str: - commit = n["commit"] - status = commit["status"] - icon = "❔" - text = "" - buildid_text = "" - if status is not None: - contexts = status["contexts"] - else: - contexts = [] - for c in contexts: - # TODO: Stop hard-coding me - if c["context"] != "ci/circleci: pytorch_linux_xenial_py3_clang5_asan_test": - continue - m = RE_CIRCLECI_URL.match(c["targetUrl"]) - if not m: - icon = "🍆" - break - if c["state"] == "SUCCESS": - icon = "✅" - break - buildid = m.group(1) - buildid_text = " ({})".format(buildid) - r = await circleci.get( - "project/github/{name}/{owner}/{buildid}".format( - buildid=buildid, **params - ) - ) - if not r["failed"]: - # It was just cancelled (don't check "cancelled"; that's - # true even if the job failed otherwise; it just means - # workflow got cancelled) - icon = "❔" - break - icon = "❌" - async with aiohttp.request( - "get", r["steps"][-1]["actions"][-1]["output_url"] - ) as resp: - log_json = await resp.json() - buf = [] - for e in log_json: - buf.append(e["message"]) - text = "\n" + strip_sccache("\n".join(buf)) - text = text[-1500:] - return "{} {} {}{}{}".format( - icon, commit["oid"][:8], commit["messageHeadline"], buildid_text, text - ) - - for n in nodes: - print(await process_node(n)) diff --git a/src/ghstack/ghstack/git.py b/src/ghstack/ghstack/git.py deleted file mode 100644 index ba1ebc4..0000000 --- a/src/ghstack/ghstack/git.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 - -import re -from functools import cached_property -from typing import List, Pattern - -import ghstack.diff -import ghstack.shell -from ghstack.types import GitCommitHash, GitTreeHash - -RE_RAW_COMMIT_ID = re.compile(r"^(?P-?)(?P[a-f0-9]+)$", re.MULTILINE) -RE_RAW_AUTHOR = re.compile( - r"^author (?P(?P[^<]+?) <(?P[^>]+)>)", re.MULTILINE -) -RE_RAW_PARENT = re.compile(r"^parent (?P[a-f0-9]+)$", re.MULTILINE) -RE_RAW_TREE = re.compile(r"^tree (?P.+)$", re.MULTILINE) -RE_RAW_COMMIT_MSG_LINE = re.compile(r"^ (?P.*)$", re.MULTILINE) - - -class CommitHeader(object): - """ - Represents the information extracted from `git rev-list --header` - """ - - # The unparsed output from git rev-list --header - raw_header: str - - def __init__(self, raw_header: str): - self.raw_header = raw_header - - def _search_group(self, regex: Pattern[str], group: str) -> str: - m = regex.search(self.raw_header) - assert m - return m.group(group) - - @cached_property - def tree(self) -> GitTreeHash: - return GitTreeHash(self._search_group(RE_RAW_TREE, "tree")) - - @cached_property - def title(self) -> str: - return self._search_group(RE_RAW_COMMIT_MSG_LINE, "line") - - @cached_property - def commit_id(self) -> GitCommitHash: - return GitCommitHash(self._search_group(RE_RAW_COMMIT_ID, "commit")) - - @cached_property - def boundary(self) -> bool: - return self._search_group(RE_RAW_COMMIT_ID, "boundary") == "-" - - @cached_property - def parents(self) -> List[GitCommitHash]: - return [ - GitCommitHash(m.group("commit")) - for m in RE_RAW_PARENT.finditer(self.raw_header) - ] - - @cached_property - def author(self) -> str: - return self._search_group(RE_RAW_AUTHOR, "author") - - @cached_property - def author_name(self) -> str: - return self._search_group(RE_RAW_AUTHOR, "name") - - @cached_property - def author_email(self) -> str: - return self._search_group(RE_RAW_AUTHOR, "email") - - @cached_property - def commit_msg(self) -> str: - return "\n".join( - m.group("line") for m in RE_RAW_COMMIT_MSG_LINE.finditer(self.raw_header) - ) - - -def split_header(s: str) -> List[CommitHeader]: - return list(map(CommitHeader, s.split("\0")[:-1])) - - -def convert_header(h: CommitHeader, github_url: str) -> ghstack.diff.Diff: - return ghstack.diff.Diff( - title=h.title, - summary=h.commit_msg, - oid=h.commit_id, - source_id=h.tree, - pull_request_resolved=ghstack.diff.PullRequestResolved.search( - h.raw_header, github_url - ), - tree=h.tree, - author_name=h.author_name, - author_email=h.author_email, - boundary=h.boundary, - ) - - -def parse_header(s: str, github_url: str) -> List[ghstack.diff.Diff]: - return [convert_header(h, github_url) for h in split_header(s)] diff --git a/src/ghstack/ghstack/github.py b/src/ghstack/ghstack/github.py deleted file mode 100644 index 31343ca..0000000 --- a/src/ghstack/ghstack/github.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 - -from abc import ABCMeta, abstractmethod -from typing import Any, Sequence - -import ghstack.diff - - -class NotFoundError(RuntimeError): - pass - - -class GitHubEndpoint(metaclass=ABCMeta): - @abstractmethod - def graphql(self, query: str, **kwargs: Any) -> Any: - """ - Args: - query: string GraphQL query to execute - **kwargs: values for variables in the graphql query - - Returns: parsed JSON response - """ - pass - - 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( - """ - query ($owner: String!, $name: String!, $number: Int!) { - repository(name: $name, owner: $owner) { - pullRequest(number: $number) { - headRefName - } - } - } - """, - **params, - ) - r = pr_result["data"]["repository"]["pullRequest"]["headRefName"] - assert isinstance(r, str), type(r) - return r - - # This hook function should be invoked when a 'git push' to GitHub - # occurs. This is used by testing to simulate actions GitHub - # takes upon branch push, more conveniently than setting up - # a branch hook on the repository and receiving events from it. - # TODO: generalize to any repo - @abstractmethod - def push_hook(self, refName: Sequence[str]) -> None: - pass - - # This should be subsumed by push_hook above, but push_hook is - # annoying to implement and this is more direct - 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.rest("get", path, **kwargs) - - def post(self, path: str, **kwargs: Any) -> Any: - """ - Send a POST request to endpoint 'path'. - - Returns: parsed JSON response - """ - return self.rest("post", path, **kwargs) - - def patch(self, path: str, **kwargs: Any) -> Any: - """ - Send a PATCH request to endpoint 'path'. - - Returns: parsed JSON response - """ - return self.rest("patch", path, **kwargs) - - @abstractmethod - 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 - """ - pass diff --git a/src/ghstack/ghstack/github_fake.py b/src/ghstack/ghstack/github_fake.py deleted file mode 100644 index d82e969..0000000 --- a/src/ghstack/ghstack/github_fake.py +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/env python3 - -import os.path -import re -from dataclasses import dataclass -from typing import Any, cast, Dict, List, NewType, Optional, Sequence - -import graphql -from typing_extensions import TypedDict - -import ghstack.diff -import ghstack.github -import ghstack.shell - -GraphQLId = NewType("GraphQLId", str) -GitHubNumber = NewType("GitHubNumber", int) -GitObjectID = NewType("GitObjectID", str) - -# https://stackoverflow.com/a/55250601 -SetDefaultBranchInput = TypedDict( - "SetDefaultBranchInput", - { - "name": str, - "default_branch": str, - }, -) - -UpdatePullRequestInput = TypedDict( - "UpdatePullRequestInput", - { - "base": Optional[str], - "title": Optional[str], - "body": Optional[str], - }, -) - -CreatePullRequestInput = TypedDict( - "CreatePullRequestInput", - { - "base": str, - "head": str, - "title": str, - "body": str, - "maintainer_can_modify": bool, - }, -) - -CreateIssueCommentInput = TypedDict( - "CreateIssueCommentInput", - {"body": str}, -) - -CreateIssueCommentPayload = TypedDict( - "CreateIssueCommentPayload", - { - "id": int, - }, -) - -UpdateIssueCommentInput = TypedDict( - "UpdateIssueCommentInput", - {"body": str}, -) - -CreatePullRequestPayload = TypedDict( - "CreatePullRequestPayload", - { - "number": int, - }, -) - - -# The "database" for our mock instance -class GitHubState: - repositories: Dict[GraphQLId, "Repository"] - pull_requests: Dict[GraphQLId, "PullRequest"] - # This is very inefficient but whatever - issue_comments: Dict[GraphQLId, "IssueComment"] - _next_id: int - # These are indexed by repo id - _next_pull_request_number: Dict[GraphQLId, int] - _next_issue_comment_full_database_id: Dict[GraphQLId, int] - root: "Root" - upstream_sh: Optional[ghstack.shell.Shell] - - def repository(self, owner: str, name: str) -> "Repository": - nameWithOwner = "{}/{}".format(owner, name) - for r in self.repositories.values(): - if r.nameWithOwner == nameWithOwner: - return r - raise RuntimeError("unknown repository {}".format(nameWithOwner)) - - def pull_request(self, repo: "Repository", number: GitHubNumber) -> "PullRequest": - for pr in self.pull_requests.values(): - if repo.id == pr._repository and pr.number == number: - return pr - raise RuntimeError( - "unrecognized pull request #{} in repository {}".format( - number, repo.nameWithOwner - ) - ) - - def issue_comment(self, repo: "Repository", comment_id: int) -> "IssueComment": - for comment in self.issue_comments.values(): - if repo.id == comment._repository and comment.fullDatabaseId == comment_id: - return comment - raise RuntimeError( - f"unrecognized issue comment {comment_id} in repository {repo.nameWithOwner}" - ) - - def next_id(self) -> GraphQLId: - r = GraphQLId(str(self._next_id)) - self._next_id += 1 - return r - - def next_pull_request_number(self, repo_id: GraphQLId) -> GitHubNumber: - r = GitHubNumber(self._next_pull_request_number[repo_id]) - self._next_pull_request_number[repo_id] += 1 - return r - - def next_issue_comment_full_database_id(self, repo_id: GraphQLId) -> int: - r = self._next_issue_comment_full_database_id[repo_id] - self._next_issue_comment_full_database_id[repo_id] += 1 - return r - - def push_hook(self, refs: Sequence[str]) -> None: - # updated_refs = set(refs) - # for pr in self.pull_requests: - # # TODO: this assumes only origin repository - # # if pr.headRefName in updated_refs: - # # pr.headRef = - # pass - pass - - def notify_merged(self, pr_resolved: ghstack.diff.PullRequestResolved) -> None: - repo = self.repository(pr_resolved.owner, pr_resolved.repo) - pr = self.pull_request(repo, GitHubNumber(pr_resolved.number)) - pr.closed = True - # TODO: model merged too - - def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: - self.repositories = {} - self.pull_requests = {} - self.issue_comments = {} - self._next_id = 5000 - self._next_pull_request_number = {} - self._next_issue_comment_full_database_id = {} - self.root = Root() - - # Populate it with the most important repo ;) - repo = Repository( - id=GraphQLId("1000"), - name="pytorch", - nameWithOwner="pytorch/pytorch", - isFork=False, - defaultBranchRef=None, - ) - self.repositories[GraphQLId("1000")] = repo - self._next_pull_request_number[GraphQLId("1000")] = 500 - self._next_issue_comment_full_database_id[GraphQLId("1000")] = 1500 - - self.upstream_sh = upstream_sh - if self.upstream_sh is not None: - # Setup upstream Git repository representing the - # pytorch/pytorch repository in the directory specified - # by upstream_sh. This is useful because some GitHub API - # 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") - 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) - - # 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") - - -@dataclass -class Node: - id: GraphQLId - - -GraphQLResolveInfo = Any # for now - - -def github_state(info: GraphQLResolveInfo) -> GitHubState: - context = info.context - assert isinstance(context, GitHubState) - return context - - -@dataclass -class Repository(Node): - name: str - nameWithOwner: str - isFork: bool - defaultBranchRef: Optional["Ref"] - - def pullRequest( - self, info: GraphQLResolveInfo, number: GitHubNumber - ) -> "PullRequest": - return github_state(info).pull_request(self, number) - - def pullRequests(self, info: GraphQLResolveInfo) -> "PullRequestConnection": - return PullRequestConnection( - nodes=list( - filter( - lambda pr: self == pr.repository(info), - github_state(info).pull_requests.values(), - ) - ) - ) - - # 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": - # TODO: Probably should preserve object identity here when - # you call this with refName/oid that are the same - assert state.upstream_sh - gitObject = GitObject( - id=state.next_id(), - # TODO: this upstream_sh hardcode wrong, but ok for now - # because we only have one repo - oid=GitObjectID(state.upstream_sh.git("rev-parse", refName)), - _repository=self.id, - ) - ref = Ref( - id=state.next_id(), - name=refName, - _repository=self.id, - target=gitObject, - ) - return ref - - -@dataclass -class GitObject(Node): - oid: GitObjectID - _repository: GraphQLId - - def repository(self, info: GraphQLResolveInfo) -> Repository: - return github_state(info).repositories[self._repository] - - -@dataclass -class Ref(Node): - name: str - _repository: GraphQLId - target: GitObject - - def repository(self, info: GraphQLResolveInfo) -> Repository: - return github_state(info).repositories[self._repository] - - -@dataclass -class PullRequest(Node): - baseRef: Optional[Ref] - baseRefName: str - body: str - closed: bool - headRef: Optional[Ref] - headRefName: str - # headRepository: Optional[Repository] - # maintainerCanModify: bool - number: GitHubNumber - _repository: GraphQLId # cycle breaker - # state: PullRequestState - title: str - url: str - - def repository(self, info: GraphQLResolveInfo) -> Repository: - return github_state(info).repositories[self._repository] - - -@dataclass -class IssueComment(Node): - body: str - fullDatabaseId: int - _repository: GraphQLId - - def repository(self, info: GraphQLResolveInfo) -> Repository: - return github_state(info).repositories[self._repository] - - -@dataclass -class PullRequestConnection: - nodes: List[PullRequest] - - -class Root: - def repository(self, info: GraphQLResolveInfo, owner: str, name: str) -> Repository: - return github_state(info).repository(owner, name) - - def node(self, info: GraphQLResolveInfo, id: GraphQLId) -> Node: - if id in github_state(info).repositories: - return github_state(info).repositories[id] - elif id in github_state(info).pull_requests: - return github_state(info).pull_requests[id] - elif id in github_state(info).issue_comments: - return github_state(info).issue_comments[id] - else: - raise RuntimeError("unknown id {}".format(id)) - - -with open( - os.path.join(os.path.dirname(__file__), "github_schema.graphql"), encoding="utf-8" -) as f: - GITHUB_SCHEMA = graphql.build_schema(f.read()) - - -# Ummm. I thought there would be a way to stick these on the objects -# themselves (in the same way resolvers can be put on resolvers) but -# after a quick read of default_resolve_type_fn it doesn't look like -# we ever actually look to value for type of information. This is -# pretty clunky lol. -def set_is_type_of(name: str, cls: Any) -> None: - # Can't use a type ignore on the next line because fbcode - # and us don't agree that it's necessary hmm. - o: Any = GITHUB_SCHEMA.get_type(name) - o.is_type_of = lambda obj, info: isinstance(obj, cls) - - -set_is_type_of("Repository", Repository) -set_is_type_of("PullRequest", PullRequest) -set_is_type_of("IssueComment", IssueComment) - - -class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint): - state: GitHubState - - 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( - schema=GITHUB_SCHEMA, - source=query, - root_value=self.state.root, - context_value=self.state, - 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: - self.state.push_hook(refNames) - - 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, - } - - # 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"] - - def _create_issue_comment( - self, owner: str, name: str, comment_id: int, input: CreateIssueCommentInput - ) -> CreateIssueCommentPayload: - state = self.state - id = state.next_id() - repo = state.repository(owner, name) - comment_id = state.next_issue_comment_full_database_id(repo.id) - comment = IssueComment( - id=id, - _repository=repo.id, - fullDatabaseId=comment_id, - body=input["body"], - ) - state.issue_comments[id] = comment - # This is only a subset of what the actual REST endpoint - # returns. - return { - "id": comment_id, - } - - def _update_issue_comment( - self, owner: str, name: str, comment_id: int, input: UpdateIssueCommentInput - ) -> None: - state = self.state - repo = state.repository(owner, name) - comment = state.issue_comment(repo, comment_id) - 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"]) - - def rest(self, method: str, path: str, **kwargs: Any) -> Any: - if method == "get": - m = re.match(r"^repos/([^/]+)/([^/]+)/branches/([^/]+)/protection", path) - if m: - # For now, pretend all branches are not protected - raise ghstack.github.NotFoundError() - - 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), - m.group(2), - GitHubNumber(int(m.group(3))), - cast(CreateIssueCommentInput, kwargs), - ) - 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), - 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/ghstack/github_real.py b/src/ghstack/ghstack/github_real.py deleted file mode 100644 index 600fe64..0000000 --- a/src/ghstack/ghstack/github_real.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 - -import json -import logging -import re -import time -from typing import Any, Dict, Optional, Sequence, Tuple, Union - -import requests - -import ghstack.github - -MAX_RETRIES = 5 -INITIAL_BACKOFF_SECONDS = 60 - - -class RealGitHubEndpoint(ghstack.github.GitHubEndpoint): - """ - A class representing a GitHub endpoint we can send queries to. - It supports both GraphQL and REST interfaces. - """ - - # The URL of the GraphQL endpoint to connect to - @property - def graphql_endpoint(self) -> str: - if self.github_url == "github.com": - return f"https://api.{self.github_url}/graphql" - else: - return f"https://{self.github_url}/api/graphql" - - # The base URL of the REST endpoint to connect to (all REST requests - # will be subpaths of this URL) - @property - def rest_endpoint(self) -> str: - if self.github_url == "github.com": - return f"https://api.{self.github_url}" - else: - return f"https://{self.github_url}/api/v3" - - # The base URL of regular WWW website, in case we need to manually - # interact with the real website - www_endpoint: str = "https://{github_url}" - - # The string OAuth token to authenticate to the GraphQL server with. - # May be None if we're doing public access only. - oauth_token: Optional[str] - - # The URL of a proxy to use for these connections (for - # Facebook users, this is typically 'http://fwdproxy:8080') - proxy: Optional[str] - - # The certificate bundle to be used to verify the connection. - # Passed to requests as 'verify'. - verify: Optional[str] - - # Client side certificate to use when connecitng. - # Passed to requests as 'cert'. - cert: Optional[Union[str, Tuple[str, str]]] - - def __init__( - self, - oauth_token: Optional[str], - github_url: str, - proxy: Optional[str] = None, - verify: Optional[str] = None, - cert: Optional[Union[str, Tuple[str, str]]] = None, - ): - self.oauth_token = oauth_token - self.proxy = proxy - self.github_url = github_url - self.verify = verify - self.cert = cert - - def push_hook(self, refName: Sequence[str]) -> None: - pass - - def graphql(self, query: str, **kwargs: Any) -> Any: - headers = {} - if self.oauth_token: - headers["Authorization"] = "bearer {}".format(self.oauth_token) - - logging.debug( - "# POST {}".format(self.graphql_endpoint.format(github_url=self.github_url)) - ) - logging.debug("Request GraphQL query:\n{}".format(query)) - logging.debug( - "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)) - - 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) - - if "errors" in r: - raise RuntimeError(pretty_json) - - 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: - return 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 - if m := re.search(r' Any: - assert self.oauth_token - headers = { - "Authorization": "token " + self.oauth_token, - "Content-Type": "application/json", - "User-Agent": "ghstack", - "Accept": "application/vnd.github.v3+json", - } - - url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path - - backoff_seconds = INITIAL_BACKOFF_SECONDS - for attempt in range(0, MAX_RETRIES): - logging.debug("# {} {}".format(method, url)) - logging.debug("Request body:\n{}".format(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("Response status: {}".format(resp.status_code)) - - try: - r = resp.json() - except ValueError: - logging.debug("Response body:\n{}".format(r.text)) - raise - else: - pretty_json = json.dumps(r, indent=1) - logging.debug("Response JSON:\n{}".format(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") diff --git a/src/ghstack/ghstack/github_schema.graphql b/src/ghstack/ghstack/github_schema.graphql deleted file mode 100644 index 436f0d7..0000000 --- a/src/ghstack/ghstack/github_schema.graphql +++ /dev/null @@ -1,9067 +0,0 @@ -# Yoinked from https://github.com/octokit/graphql-schema -# at commit c5dac09a12478fbd51b1df2e7261067e79afd81c - -# Copyright (c) 2017 Gregor Martynus -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -# Autogenerated input type of AcceptTopicSuggestion -input AcceptTopicSuggestionInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of the suggested topic. - name: String! - - # The Node ID of the repository. - repositoryId: ID! -} - -# Autogenerated return type of AcceptTopicSuggestion -type AcceptTopicSuggestionPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The accepted topic. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `topic` will change from `Topic!` to `Topic`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - topic: Topic! -} - -# Represents an object which can take actions on GitHub. Typically a User or Bot. -interface Actor { - # A URL pointing to the actor's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - - # The username of the actor. - login: String! - - # The HTTP path for this actor. - resourcePath: URI! - - # The HTTP URL for this actor. - url: URI! -} - -# Autogenerated input type of AddComment -input AddCommentInput { - # The contents of the comment. - body: String! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the subject to modify. - subjectId: ID! -} - -# Autogenerated return type of AddComment -type AddCommentPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The edge from the subject's comment connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `commentEdge` will change from `IssueCommentEdge!` to `IssueCommentEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - commentEdge: IssueCommentEdge! - - # The subject - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `subject` will change from `Node!` to `Node`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - subject: Node! - - # The edge from the subject's timeline connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `timelineEdge` will change from `IssueTimelineItemEdge!` to `IssueTimelineItemEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - timelineEdge: IssueTimelineItemEdge! -} - -# Autogenerated input type of AddProjectCard -input AddProjectCardInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The content of the card. Must be a member of the ProjectCardItem union - contentId: ID - - # The note on the card. - note: String - - # The Node ID of the ProjectColumn. - projectColumnId: ID! -} - -# Autogenerated return type of AddProjectCard -type AddProjectCardPayload { - # The edge from the ProjectColumn's card connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - cardEdge: ProjectCardEdge! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The ProjectColumn - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `projectColumn` will change from `Project!` to `Project`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - projectColumn: Project! -} - -# Autogenerated input type of AddProjectColumn -input AddProjectColumnInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of the column. - name: String! - - # The Node ID of the project. - projectId: ID! -} - -# Autogenerated return type of AddProjectColumn -type AddProjectColumnPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The edge from the project's column connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - columnEdge: ProjectColumnEdge! - - # The project - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `project` will change from `Project!` to `Project`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - project: Project! -} - -# Autogenerated input type of AddPullRequestReviewComment -input AddPullRequestReviewCommentInput { - # The text of the comment. - body: String! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The SHA of the commit to comment on. - commitOID: GitObjectID - - # The comment id to reply to. - inReplyTo: ID - - # The relative path of the file to comment on. - path: String - - # The line index in the diff to comment on. - position: Int - - # The Node ID of the review to modify. - pullRequestReviewId: ID! -} - -# Autogenerated return type of AddPullRequestReviewComment -type AddPullRequestReviewCommentPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The newly created comment. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `comment` will change from `PullRequestReviewComment!` to `PullRequestReviewComment`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - comment: PullRequestReviewComment! - - # The edge from the review's comment connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `commentEdge` will change from - # `PullRequestReviewCommentEdge!` to `PullRequestReviewCommentEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - commentEdge: PullRequestReviewCommentEdge! -} - -# Autogenerated input type of AddPullRequestReview -input AddPullRequestReviewInput { - # The contents of the review body comment. - body: String - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The review line comments. - comments: [DraftPullRequestReviewComment] - - # The commit OID the review pertains to. - commitOID: GitObjectID - - # The event to perform on the pull request review. - event: PullRequestReviewEvent - - # The Node ID of the pull request to modify. - pullRequestId: ID! -} - -# Autogenerated return type of AddPullRequestReview -type AddPullRequestReviewPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The newly created pull request review. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReview: PullRequestReview! - - # The edge from the pull request's review connection. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `reviewEdge` will change from `PullRequestReviewEdge!` to `PullRequestReviewEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - reviewEdge: PullRequestReviewEdge! -} - -# Autogenerated input type of AddReaction -input AddReactionInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of the emoji to react with. - content: ReactionContent! - - # The Node ID of the subject to modify. - subjectId: ID! -} - -# Autogenerated return type of AddReaction -type AddReactionPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The reaction object. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - reaction: Reaction! - - # The reactable subject. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - subject: Reactable! -} - -# Autogenerated input type of AddStar -input AddStarInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Starrable ID to star. - starrableId: ID! -} - -# Autogenerated return type of AddStar -type AddStarPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The starrable. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - starrable: Starrable! -} - -# Represents a 'added_to_project' event on a given issue or pull request. -type AddedToProjectEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# A GitHub App. -type App implements Node { - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - - # The description of the app. - description: String - id: ID! - - # The hex color code, without the leading '#', for the logo background. - logoBackgroundColor: String! - - # A URL pointing to the app's logo. - logoUrl( - # The size of the resulting image. - size: Int - ): URI! - - # The name of the app. - name: String! - - # A slug based on the name of the app for use in URLs. - slug: String! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The URL to the app's homepage. - url: URI! -} - -# An edge in a connection. -type AppEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: App -} - -# An object that can have users assigned to it. -interface Assignable { - # A list of Users assigned to this object. - assignees( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! -} - -# Represents an 'assigned' event on any assignable object. -type AssignedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the assignable associated with the event. - assignable: Assignable! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the user who was assigned. - user: User -} - -# Represents a 'base_ref_changed' event on a given issue or pull request. -type BaseRefChangedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# Represents a 'base_ref_force_pushed' event on a given pull request. -type BaseRefForcePushedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the after commit SHA for the 'base_ref_force_pushed' event. - afterCommit: Commit - - # Identifies the before commit SHA for the 'base_ref_force_pushed' event. - beforeCommit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # Identifies the fully qualified ref name for the 'base_ref_force_pushed' event. - ref: Ref -} - -# Represents a Git blame. -type Blame { - # The list of ranges from a Git blame. - ranges: [BlameRange!]! -} - -# Represents a range of information from a Git blame. -type BlameRange { - # Identifies the recency of the change, from 1 (new) to 10 (old). This is - # calculated as a 2-quantile and determines the length of distance between the - # median age of all the changes in the file and the recency of the current - # range's change. - age: Int! - - # Identifies the line author - commit: Commit! - - # The ending line for the range - endingLine: Int! - - # The starting line for the range - startingLine: Int! -} - -# Represents a Git blob. -type Blob implements GitObject & Node { - # An abbreviated version of the Git object ID - abbreviatedOid: String! - - # Byte size of Blob object - byteSize: Int! - - # The HTTP path for this Git object - commitResourcePath: URI! - - # The HTTP URL for this Git object - commitUrl: URI! - id: ID! - - # Indicates whether the Blob is binary or text - isBinary: Boolean! - - # Indicates whether the contents is truncated - isTruncated: Boolean! - - # The Git object ID - oid: GitObjectID! - - # The Repository the Git object belongs to - repository: Repository! - - # UTF8 text data or null if the Blob is binary - text: String -} - -# A special type of user which takes actions on behalf of GitHub Apps. -type Bot implements Actor & Node & UniformResourceLocatable { - # A URL pointing to the GitHub App's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # The username of the actor. - login: String! - - # The HTTP path for this bot - resourcePath: URI! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this bot - url: URI! -} - -# An object that can be closed -interface Closable { - # `true` if the object is closed (definition of closed may depend on type) - closed: Boolean! - - # Identifies the date and time when the object was closed. - closedAt: DateTime -} - -# Represents a 'closed' event on any `Closable`. -type ClosedEvent implements Node & UniformResourceLocatable { - # Identifies the actor who performed the event. - actor: Actor - - # Object that was closed. - closable: Closable! - - # Object which triggered the creation of this event. - closer: Closer - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # The HTTP path for this closed event. - resourcePath: URI! - - # The HTTP URL for this closed event. - url: URI! -} - -# The object which triggered a `ClosedEvent`. -union Closer = Commit | PullRequest - -# The Code of Conduct for a repository -type CodeOfConduct { - # The body of the CoC - body: String - - # The key for the CoC - key: String! - - # The formal name of the CoC - name: String! - - # The path to the CoC - url: URI -} - -# Collaborators affiliation level with a subject. -enum CollaboratorAffiliation { - # All collaborators the authenticated user can see. - ALL - - # All collaborators with permissions to an organization-owned subject, regardless of organization membership status. - DIRECT - - # All outside collaborators of an organization-owned subject. - OUTSIDE -} - -# Types that can be inside Collection Items. -union CollectionItemContent = Organization | Repository | User - -# Represents a comment. -interface Comment { - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # The body as Markdown. - body: String! - - # The body rendered to HTML. - bodyHTML: HTML! - - # The body rendered to text. - bodyText: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies when the comment was published at. - publishedAt: DateTime - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# A comment author association with repository. -enum CommentAuthorAssociation { - # Author has been invited to collaborate on the repository. - COLLABORATOR - - # Author has previously committed to the repository. - CONTRIBUTOR - - # Author has not previously committed to GitHub. - FIRST_TIMER - - # Author has not previously committed to the repository. - FIRST_TIME_CONTRIBUTOR - - # Author is a member of the organization that owns the repository. - MEMBER - - # Author has no association with the repository. - NONE - - # Author is the owner of the repository. - OWNER -} - -# The possible errors that will prevent a user from updating a comment. -enum CommentCannotUpdateReason { - # You must be the author or have write access to this repository to update this comment. - INSUFFICIENT_ACCESS - - # Unable to create comment because issue is locked. - LOCKED - - # You must be logged in to update this comment. - LOGIN_REQUIRED - - # Repository is under maintenance. - MAINTENANCE - - # At least one email address must be verified to update this comment. - VERIFIED_EMAIL_REQUIRED -} - -# Represents a 'comment_deleted' event on a given issue or pull request. -type CommentDeletedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# Represents a Git commit. -type Commit implements GitObject & Node & Subscribable { - # An abbreviated version of the Git object ID - abbreviatedOid: String! - - # The number of additions in this commit. - additions: Int! - - # Authorship details of the commit. - author: GitActor - - # Check if the committer and the author match. - authoredByCommitter: Boolean! - - # The datetime when this commit was authored. - authoredDate: DateTime! - - # Fetches `git blame` information. - blame( - # The file whose Git blame information you want. - path: String! - ): Blame! - - # The number of changed files in this commit. - changedFiles: Int! - - # Comments made on the commit. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): CommitCommentConnection! - - # The HTTP path for this Git object - commitResourcePath: URI! - - # The HTTP URL for this Git object - commitUrl: URI! - - # The datetime when this commit was committed. - committedDate: DateTime! - - # Check if commited via GitHub web UI. - committedViaWeb: Boolean! - - # Committership details of the commit. - committer: GitActor - - # The number of deletions in this commit. - deletions: Int! - - # The linear commit history starting from (and including) this commit, in the same order as `git log`. - history( - # Returns the elements in the list that come after the specified cursor. - after: String - - # If non-null, filters history to only show commits with matching authorship. - author: CommitAuthor - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # If non-null, filters history to only show commits touching files under this path. - path: String - - # Allows specifying a beginning time or date for fetching commits. - since: GitTimestamp - - # Allows specifying an ending time or date for fetching commits. - until: GitTimestamp - ): CommitHistoryConnection! - id: ID! - - # The Git commit message - message: String! - - # The Git commit message body - messageBody: String! - - # The commit message body rendered to HTML. - messageBodyHTML: HTML! - - # The Git commit message headline - messageHeadline: String! - - # The commit message headline rendered to HTML. - messageHeadlineHTML: HTML! - - # The Git object ID - oid: GitObjectID! - - # The parents of a commit. - parents( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): CommitConnection! - - # The datetime when this commit was pushed. - pushedDate: DateTime - - # The Repository this commit belongs to - repository: Repository! - - # The HTTP path for this commit - resourcePath: URI! - - # Commit signing information, if present. - signature: GitSignature - - # Status information for this commit - status: Status - - # Returns a URL to download a tarball archive for a repository. - # Note: For private repositories, these links are temporary and expire after five minutes. - tarballUrl: URI! - - # Commit's root Tree - tree: Tree! - - # The HTTP path for the tree of this commit - treeResourcePath: URI! - - # The HTTP URL for the tree of this commit - treeUrl: URI! - - # The HTTP URL for this commit - url: URI! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState - - # Returns a URL to download a zipball archive for a repository. - # Note: For private repositories, these links are temporary and expire after five minutes. - zipballUrl: URI! -} - -# Specifies an author for filtering Git commits. -input CommitAuthor { - # Email addresses to filter by. Commits authored by any of the specified email addresses will be returned. - emails: [String!] - - # ID of a User to filter by. If non-null, only commits authored by this user - # will be returned. This field takes precedence over emails. - id: ID -} - -# Represents a comment on a given Commit. -type CommitComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # Identifies the comment body. - body: String! - - # Identifies the comment body rendered to HTML. - bodyHTML: HTML! - - # The body rendered to text. - bodyText: String! - - # Identifies the commit associated with the comment, if the commit exists. - commit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies the file path associated with the comment. - path: String - - # Identifies the line position associated with the comment. - position: Int - - # Identifies when the comment was published at. - publishedAt: DateTime - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # The repository associated with this node. - repository: Repository! - - # The HTTP path permalink for this commit comment. - resourcePath: URI! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL permalink for this commit comment. - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! - - # Can user react to this subject - viewerCanReact: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# The connection type for CommitComment. -type CommitCommentConnection { - # A list of edges. - edges: [CommitCommentEdge] - - # A list of nodes. - nodes: [CommitComment] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type CommitCommentEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: CommitComment -} - -# A thread of comments on a commit. -type CommitCommentThread implements Node & RepositoryNode { - # The comments that exist in this thread. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): CommitCommentConnection! - - # The commit the comments were made on. - commit: Commit! - id: ID! - - # The file the comments were made on. - path: String - - # The position in the diff for the commit that the comment was made on. - position: Int - - # The repository associated with this node. - repository: Repository! -} - -# The connection type for Commit. -type CommitConnection { - # A list of edges. - edges: [CommitEdge] - - # A list of nodes. - nodes: [Commit] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type CommitEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Commit -} - -# The connection type for Commit. -type CommitHistoryConnection { - edges: [CommitEdge] - - # A list of nodes. - nodes: [Commit] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a 'converted_note_to_issue' event on a given issue or pull request. -type ConvertedNoteToIssueEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# Autogenerated input type of CreateProject -input CreateProjectInput { - # The description of project. - body: String - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of project. - name: String! - - # The owner ID to create the project under. - ownerId: ID! -} - -# Autogenerated return type of CreateProject -type CreateProjectPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The new project. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `project` will change from `Project!` to `Project`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - project: Project! -} - -# Represents a mention made by one issue or pull request to another. -type CrossReferencedEvent implements Node & UniformResourceLocatable { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Reference originated in a different repository. - isCrossRepository: Boolean! - - # Identifies when the reference was made. - referencedAt: DateTime! - - # The HTTP path for this pull request. - resourcePath: URI! - - # Issue or pull request that made the reference. - source: ReferencedSubject! - - # Issue or pull request to which the reference was made. - target: ReferencedSubject! - - # The HTTP URL for this pull request. - url: URI! - - # Checks if the target will be closed when the source is merged. - willCloseTarget: Boolean! -} - -# An ISO-8601 encoded date string. -scalar Date - -# An ISO-8601 encoded UTC date string. -scalar DateTime - -# Autogenerated input type of DeclineTopicSuggestion -input DeclineTopicSuggestionInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of the suggested topic. - name: String! - - # The reason why the suggested topic is declined. - reason: TopicSuggestionDeclineReason! - - # The Node ID of the repository. - repositoryId: ID! -} - -# Autogenerated return type of DeclineTopicSuggestion -type DeclineTopicSuggestionPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The declined topic. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `topic` will change from `Topic!` to `Topic`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - topic: Topic! -} - -# The possible default permissions for repositories. -enum DefaultRepositoryPermissionField { - # Can read, write, and administrate repos by default - ADMIN - - # No access - NONE - - # Can read repos by default - READ - - # Can read and write repos by default - WRITE -} - -# Entities that can be deleted. -interface Deletable { - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! -} - -# Autogenerated input type of DeleteProjectCard -input DeleteProjectCardInput { - # The id of the card to delete. - cardId: ID! - - # A unique identifier for the client performing the mutation. - clientMutationId: String -} - -# Autogenerated return type of DeleteProjectCard -type DeleteProjectCardPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The column the deleted card was in. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `column` will change from `ProjectColumn!` to `ProjectColumn`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - column: ProjectColumn! - - # The deleted card ID. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `deletedCardId` will change from `ID!` to `ID`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - deletedCardId: ID! -} - -# Autogenerated input type of DeleteProjectColumn -input DeleteProjectColumnInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The id of the column to delete. - columnId: ID! -} - -# Autogenerated return type of DeleteProjectColumn -type DeleteProjectColumnPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The deleted column ID. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `deletedColumnId` will change from `ID!` to `ID`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - deletedColumnId: ID! - - # The project the deleted column was in. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `project` will change from `Project!` to `Project`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - project: Project! -} - -# Autogenerated input type of DeleteProject -input DeleteProjectInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Project ID to update. - projectId: ID! -} - -# Autogenerated return type of DeleteProject -type DeleteProjectPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The repository or organization the project was removed from. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `owner` will change from `ProjectOwner!` to `ProjectOwner`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - owner: ProjectOwner! -} - -# Autogenerated input type of DeletePullRequestReview -input DeletePullRequestReviewInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the pull request review to delete. - pullRequestReviewId: ID! -} - -# Autogenerated return type of DeletePullRequestReview -type DeletePullRequestReviewPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The deleted pull request review. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReview: PullRequestReview! -} - -# Represents a 'demilestoned' event on a given issue or pull request. -type DemilestonedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the milestone title associated with the 'demilestoned' event. - milestoneTitle: String! - - # Object referenced by event. - subject: MilestoneItem! -} - -# A repository deploy key. -type DeployKey implements Node { - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # The deploy key. - key: String! - - # Whether or not the deploy key is read only. - readOnly: Boolean! - - # The deploy key title. - title: String! - - # Whether or not the deploy key has been verified. - verified: Boolean! -} - -# The connection type for DeployKey. -type DeployKeyConnection { - # A list of edges. - edges: [DeployKeyEdge] - - # A list of nodes. - nodes: [DeployKey] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type DeployKeyEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: DeployKey -} - -# Represents a 'deployed' event on a given pull request. -type DeployedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - - # The deployment associated with the 'deployed' event. - deployment: Deployment! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # The ref associated with the 'deployed' event. - ref: Ref -} - -# Represents triggered deployment instance. -type Deployment implements Node { - # Identifies the commit sha of the deployment. - commit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the actor who triggered the deployment. - creator: Actor - - # Identifies the primary key from the database. - databaseId: Int - - # The deployment description. - description: String - - # The environment to which this deployment was made. - environment: String - id: ID! - - # The latest status of this deployment. - latestStatus: DeploymentStatus - - # Extra information that a deployment system might need. - payload: String - - # Identifies the Ref of the deployment, if the deployment was created by ref. - ref: Ref - - # Identifies the repository associated with the deployment. - repository: Repository! - - # The current state of the deployment. - state: DeploymentState - - # A list of statuses associated with the deployment. - statuses( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): DeploymentStatusConnection - - # The deployment task. - task: String - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! -} - -# The connection type for Deployment. -type DeploymentConnection { - # A list of edges. - edges: [DeploymentEdge] - - # A list of nodes. - nodes: [Deployment] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type DeploymentEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Deployment -} - -# Represents a 'deployment_environment_changed' event on a given pull request. -type DeploymentEnvironmentChangedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The deployment status that updated the deployment environment. - deploymentStatus: DeploymentStatus! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! -} - -# The possible states in which a deployment can be. -enum DeploymentState { - # The pending deployment was not updated after 30 minutes. - ABANDONED - - # The deployment is currently active. - ACTIVE - - # An inactive transient deployment. - DESTROYED - - # The deployment experienced an error. - ERROR - - # The deployment has failed. - FAILURE - - # The deployment is inactive. - INACTIVE - - # The deployment is pending. - PENDING -} - -# Describes the status of a given deployment attempt. -type DeploymentStatus implements Node { - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the actor who triggered the deployment. - creator: Actor - - # Identifies the deployment associated with status. - deployment: Deployment! - - # Identifies the description of the deployment. - description: String - - # Identifies the environment URL of the deployment. - environmentUrl: URI - id: ID! - - # Identifies the log URL of the deployment. - logUrl: URI - - # Identifies the current state of the deployment. - state: DeploymentStatusState! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! -} - -# The connection type for DeploymentStatus. -type DeploymentStatusConnection { - # A list of edges. - edges: [DeploymentStatusEdge] - - # A list of nodes. - nodes: [DeploymentStatus] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type DeploymentStatusEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: DeploymentStatus -} - -# The possible states for a deployment status. -enum DeploymentStatusState { - # The deployment experienced an error. - ERROR - - # The deployment has failed. - FAILURE - - # The deployment is inactive. - INACTIVE - - # The deployment is pending. - PENDING - - # The deployment was successful. - SUCCESS -} - -# Autogenerated input type of DismissPullRequestReview -input DismissPullRequestReviewInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The contents of the pull request review dismissal message. - message: String! - - # The Node ID of the pull request review to modify. - pullRequestReviewId: ID! -} - -# Autogenerated return type of DismissPullRequestReview -type DismissPullRequestReviewPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The dismissed pull request review. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReview: PullRequestReview! -} - -# Specifies a review comment to be left with a Pull Request Review. -input DraftPullRequestReviewComment { - # Body of the comment to leave. - body: String! - - # Path to the file being commented on. - path: String! - - # Position in the file to leave a comment on. - position: Int! -} - -# An external identity provisioned by SAML SSO or SCIM. -type ExternalIdentity implements Node { - # The GUID for this identity - guid: String! - id: ID! - - # Organization invitation for this SCIM-provisioned external identity - organizationInvitation: OrganizationInvitation - - # SAML Identity attributes - samlIdentity: ExternalIdentitySamlAttributes - - # SCIM Identity attributes - scimIdentity: ExternalIdentityScimAttributes - - # User linked to this external identity. Will be NULL if this identity has not been claimed by an organization member. - user: User -} - -# The connection type for ExternalIdentity. -type ExternalIdentityConnection { - # A list of edges. - edges: [ExternalIdentityEdge] - - # A list of nodes. - nodes: [ExternalIdentity] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ExternalIdentityEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ExternalIdentity -} - -# SAML attributes for the External Identity -type ExternalIdentitySamlAttributes { - # The NameID of the SAML identity - nameId: String -} - -# SCIM attributes for the External Identity -type ExternalIdentityScimAttributes { - # The userName of the SCIM identity - username: String -} - -# The connection type for User. -type FollowerConnection { - # A list of edges. - edges: [UserEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# The connection type for User. -type FollowingConnection { - # A list of edges. - edges: [UserEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# A Gist. -type Gist implements Node & Starrable { - # A list of comments associated with the gist - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): GistCommentConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The gist description. - description: String - id: ID! - - # Whether the gist is public or not. - isPublic: Boolean! - - # The gist name. - name: String! - - # The gist owner. - owner: RepositoryOwner - - # Identifies when the gist was last pushed to. - pushedAt: DateTime - - # A list of users who have starred this starrable. - stargazers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: StarOrder - ): StargazerConnection! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # Returns a boolean indicating whether the viewing user has starred this starrable. - viewerHasStarred: Boolean! -} - -# Represents a comment on an Gist. -type GistComment implements Comment & Deletable & Node & Updatable & UpdatableComment { - # The actor who authored the comment. - author: Actor - - # Author's association with the gist. - authorAssociation: CommentAuthorAssociation! - - # Identifies the comment body. - body: String! - - # The comment body rendered to HTML. - bodyHTML: HTML! - - # The body rendered to text. - bodyText: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The actor who edited the comment. - editor: Actor - - # The associated gist. - gist: Gist! - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies when the comment was published at. - publishedAt: DateTime - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# The connection type for GistComment. -type GistCommentConnection { - # A list of edges. - edges: [GistCommentEdge] - - # A list of nodes. - nodes: [GistComment] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type GistCommentEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: GistComment -} - -# The connection type for Gist. -type GistConnection { - # A list of edges. - edges: [GistEdge] - - # A list of nodes. - nodes: [Gist] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type GistEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Gist -} - -# Ordering options for gist connections -input GistOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order repositories by. - field: GistOrderField! -} - -# Properties by which gist connections can be ordered. -enum GistOrderField { - # Order gists by creation time - CREATED_AT - - # Order gists by push time - PUSHED_AT - - # Order gists by update time - UPDATED_AT -} - -# The privacy of a Gist -enum GistPrivacy { - # Gists that are public and secret - ALL - - # Public - PUBLIC - - # Secret - SECRET -} - -# Represents an actor in a Git commit (ie. an author or committer). -type GitActor { - # A URL pointing to the author's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - - # The timestamp of the Git action (authoring or committing). - date: GitTimestamp - - # The email in the Git commit. - email: String - - # The name in the Git commit. - name: String - - # The GitHub user corresponding to the email field. Null if no such user exists. - user: User -} - -# Represents information about the GitHub instance. -type GitHubMetadata { - # Returns a String that's a SHA of `github-services` - gitHubServicesSha: GitObjectID! - - # IP addresses that users connect to for git operations - gitIpAddresses: [String!] - - # IP addresses that service hooks are sent from - hookIpAddresses: [String!] - - # IP addresses that the importer connects from - importerIpAddresses: [String!] - - # Whether or not users are verified - isPasswordAuthenticationVerifiable: Boolean! - - # IP addresses for GitHub Pages' A records - pagesIpAddresses: [String!] -} - -# Represents a Git object. -interface GitObject { - # An abbreviated version of the Git object ID - abbreviatedOid: String! - - # The HTTP path for this Git object - commitResourcePath: URI! - - # The HTTP URL for this Git object - commitUrl: URI! - id: ID! - - # The Git object ID - oid: GitObjectID! - - # The Repository the Git object belongs to - repository: Repository! -} - -# A Git object ID. -scalar GitObjectID - -# Git SSH string -scalar GitSSHRemote - -# Information about a signature (GPG or S/MIME) on a Commit or Tag. -interface GitSignature { - # Email used to sign this object. - email: String! - - # True if the signature is valid and verified by GitHub. - isValid: Boolean! - - # Payload for GPG signing object. Raw ODB object without the signature header. - payload: String! - - # ASCII-armored signature header from object. - signature: String! - - # GitHub user corresponding to the email signing this commit. - signer: User - - # The state of this signature. `VALID` if signature is valid and verified by - # GitHub, otherwise represents reason why signature is considered invalid. - state: GitSignatureState! - - # True if the signature was made with GitHub's signing key. - wasSignedByGitHub: Boolean! -} - -# The state of a Git signature. -enum GitSignatureState { - # The signing certificate or its chain could not be verified - BAD_CERT - - # Invalid email used for signing - BAD_EMAIL - - # Signing key expired - EXPIRED_KEY - - # Internal error - the GPG verification service misbehaved - GPGVERIFY_ERROR - - # Internal error - the GPG verification service is unavailable at the moment - GPGVERIFY_UNAVAILABLE - - # Invalid signature - INVALID - - # Malformed signature - MALFORMED_SIG - - # The usage flags for the key that signed this don't allow signing - NOT_SIGNING_KEY - - # Email used for signing not known to GitHub - NO_USER - - # Valid siganture, though certificate revocation check failed - OCSP_ERROR - - # Valid signature, pending certificate revocation checking - OCSP_PENDING - - # One or more certificates in chain has been revoked - OCSP_REVOKED - - # Key used for signing not known to GitHub - UNKNOWN_KEY - - # Unknown signature type - UNKNOWN_SIG_TYPE - - # Unsigned - UNSIGNED - - # Email used for signing unverified on GitHub - UNVERIFIED_EMAIL - - # Valid signature and verified by GitHub - VALID -} - -# An ISO-8601 encoded date string. Unlike the DateTime type, GitTimestamp is not converted in UTC. -scalar GitTimestamp - -# Represents a GPG signature on a Commit or Tag. -type GpgSignature implements GitSignature { - # Email used to sign this object. - email: String! - - # True if the signature is valid and verified by GitHub. - isValid: Boolean! - - # Hex-encoded ID of the key that signed this object. - keyId: String - - # Payload for GPG signing object. Raw ODB object without the signature header. - payload: String! - - # ASCII-armored signature header from object. - signature: String! - - # GitHub user corresponding to the email signing this commit. - signer: User - - # The state of this signature. `VALID` if signature is valid and verified by - # GitHub, otherwise represents reason why signature is considered invalid. - state: GitSignatureState! - - # True if the signature was made with GitHub's signing key. - wasSignedByGitHub: Boolean! -} - -# A string containing HTML code. -scalar HTML - -# Represents a 'head_ref_deleted' event on a given pull request. -type HeadRefDeletedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the Ref associated with the `head_ref_deleted` event. - headRef: Ref - - # Identifies the name of the Ref associated with the `head_ref_deleted` event. - headRefName: String! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! -} - -# Represents a 'head_ref_force_pushed' event on a given pull request. -type HeadRefForcePushedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the after commit SHA for the 'head_ref_force_pushed' event. - afterCommit: Commit - - # Identifies the before commit SHA for the 'head_ref_force_pushed' event. - beforeCommit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # Identifies the fully qualified ref name for the 'head_ref_force_pushed' event. - ref: Ref -} - -# Represents a 'head_ref_restored' event on a given pull request. -type HeadRefRestoredEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! -} - -# An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. -type Issue implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { - # Reason that the conversation was locked. - activeLockReason: LockReason - - # A list of Users assigned to this object. - assignees( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # Identifies the body of the issue. - body: String! - - # Identifies the body of the issue rendered to HTML. - bodyHTML: HTML! - - # Identifies the body of the issue rendered to text. - bodyText: String! - - # `true` if the object is closed (definition of closed may depend on type) - closed: Boolean! - - # Identifies the date and time when the object was closed. - closedAt: DateTime - - # A list of comments associated with the Issue. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): IssueCommentConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # A list of labels associated with the object. - labels( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): LabelConnection - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # `true` if the object is locked - locked: Boolean! - - # Identifies the milestone associated with the issue. - milestone: Milestone - - # Identifies the issue number. - number: Int! - - # A list of Users that are participating in the Issue conversation. - participants( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # List of project cards associated with this issue. - projectCards( - # Returns the elements in the list that come after the specified cursor. - after: String - - # A list of archived states to filter the cards by - archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProjectCardConnection! - - # Identifies when the comment was published at. - publishedAt: DateTime - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # The repository associated with this node. - repository: Repository! - - # The HTTP path for this issue - resourcePath: URI! - - # Identifies the state of the issue. - state: IssueState! - - # A list of events, comments, commits, etc. associated with the issue. - timeline( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows filtering timeline events by a `since` timestamp. - since: DateTime - ): IssueTimelineConnection! - - # Identifies the issue title. - title: String! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this issue - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Can user react to this subject - viewerCanReact: Boolean! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState -} - -# Represents a comment on an Issue. -type IssueComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # The body as Markdown. - body: String! - - # The body rendered to HTML. - bodyHTML: HTML! - - # The body rendered to text. - bodyText: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # Identifies the issue associated with the comment. - issue: Issue! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies when the comment was published at. - publishedAt: DateTime - - # Returns the pull request associated with the comment, if this comment was made on a - # pull request. - pullRequest: PullRequest - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # The repository associated with this node. - repository: Repository! - - # The HTTP path for this issue comment - resourcePath: URI! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this issue comment - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! - - # Can user react to this subject - viewerCanReact: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# The connection type for IssueComment. -type IssueCommentConnection { - # A list of edges. - edges: [IssueCommentEdge] - - # A list of nodes. - nodes: [IssueComment] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type IssueCommentEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: IssueComment -} - -# The connection type for Issue. -type IssueConnection { - # A list of edges. - edges: [IssueEdge] - - # A list of nodes. - nodes: [Issue] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type IssueEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Issue -} - -# Used for return value of Repository.issueOrPullRequest. -union IssueOrPullRequest = Issue | PullRequest - -# Ways in which lists of issues can be ordered upon return. -input IssueOrder { - # The direction in which to order issues by the specified field. - direction: OrderDirection! - - # The field in which to order issues by. - field: IssueOrderField! -} - -# Properties by which issue connections can be ordered. -enum IssueOrderField { - # Order issues by comment count - COMMENTS - - # Order issues by creation time - CREATED_AT - - # Order issues by update time - UPDATED_AT -} - -# The possible PubSub channels for an issue. -enum IssuePubSubTopic { - # The channel ID for marking an issue as read. - MARKASREAD - - # The channel ID for observing issue state updates. - STATE - - # The channel ID for updating items on the issue timeline. - TIMELINE - - # The channel ID for observing issue updates. - UPDATED -} - -# The possible states of an issue. -enum IssueState { - # An issue that has been closed - CLOSED - - # An issue that is still open - OPEN -} - -# The connection type for IssueTimelineItem. -type IssueTimelineConnection { - # A list of edges. - edges: [IssueTimelineItemEdge] - - # A list of nodes. - nodes: [IssueTimelineItem] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An item in an issue timeline -union IssueTimelineItem = AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent | DemilestonedEvent | IssueComment | LabeledEvent | LockedEvent | MilestonedEvent | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent - -# An edge in a connection. -type IssueTimelineItemEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: IssueTimelineItem -} - -# An item in an issue timeline -union IssueTimelineItems = AddedToProjectEvent | AssignedEvent | ClosedEvent | CommentDeletedEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | IssueComment | LabeledEvent | LockedEvent | MentionedEvent | MilestonedEvent | MovedColumnsInProjectEvent | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent - -# An edge in a connection. -type IssueTimelineItemsEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: IssueTimelineItems -} - -# The possible item types found in a timeline. -enum IssueTimelineItemsItemType { - # Represents a 'added_to_project' event on a given issue or pull request. - ADDED_TO_PROJECT_EVENT - - # Represents an 'assigned' event on any assignable object. - ASSIGNED_EVENT - - # Represents a 'closed' event on any `Closable`. - CLOSED_EVENT - - # Represents a 'comment_deleted' event on a given issue or pull request. - COMMENT_DELETED_EVENT - - # Represents a 'converted_note_to_issue' event on a given issue or pull request. - CONVERTED_NOTE_TO_ISSUE_EVENT - - # Represents a mention made by one issue or pull request to another. - CROSS_REFERENCED_EVENT - - # Represents a 'demilestoned' event on a given issue or pull request. - DEMILESTONED_EVENT - - # Represents a comment on an Issue. - ISSUE_COMMENT - - # Represents a 'labeled' event on a given issue or pull request. - LABELED_EVENT - - # Represents a 'locked' event on a given issue or pull request. - LOCKED_EVENT - - # Represents a 'mentioned' event on a given issue or pull request. - MENTIONED_EVENT - - # Represents a 'milestoned' event on a given issue or pull request. - MILESTONED_EVENT - - # Represents a 'moved_columns_in_project' event on a given issue or pull request. - MOVED_COLUMNS_IN_PROJECT_EVENT - - # Represents a 'referenced' event on a given `ReferencedSubject`. - REFERENCED_EVENT - - # Represents a 'removed_from_project' event on a given issue or pull request. - REMOVED_FROM_PROJECT_EVENT - - # Represents a 'renamed' event on a given issue or pull request - RENAMED_TITLE_EVENT - - # Represents a 'reopened' event on any `Closable`. - REOPENED_EVENT - - # Represents a 'subscribed' event on a given `Subscribable`. - SUBSCRIBED_EVENT - - # Represents an 'unassigned' event on any assignable object. - UNASSIGNED_EVENT - - # Represents an 'unlabeled' event on a given issue or pull request. - UNLABELED_EVENT - - # Represents an 'unlocked' event on a given issue or pull request. - UNLOCKED_EVENT - - # Represents an 'unsubscribed' event on a given `Subscribable`. - UNSUBSCRIBED_EVENT -} - -# A label for categorizing Issues or Milestones with a given Repository. -type Label implements Node { - # Identifies the label color. - color: String! - - # Identifies the date and time when the label was created. - createdAt: DateTime - - # A brief description of this label. - description: String - id: ID! - - # Indicates whether or not this is a default label. - isDefault: Boolean! - - # A list of issues associated with this label. - issues( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for issues returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the issues by. - states: [IssueState!] - ): IssueConnection! - - # Identifies the label name. - name: String! - - # A list of pull requests associated with this label. - pullRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # The base ref name to filter the pull requests by. - baseRefName: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # The head ref name to filter the pull requests by. - headRefName: String - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for pull requests returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the pull requests by. - states: [PullRequestState!] - ): PullRequestConnection! - - # The repository associated with this label. - repository: Repository! - - # The HTTP path for this label. - resourcePath: URI! - - # Identifies the date and time when the label was last updated. - updatedAt: DateTime - - # The HTTP URL for this label. - url: URI! -} - -# The connection type for Label. -type LabelConnection { - # A list of edges. - edges: [LabelEdge] - - # A list of nodes. - nodes: [Label] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type LabelEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Label -} - -# An object that can have labels assigned to it. -interface Labelable { - # A list of labels associated with the object. - labels( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): LabelConnection -} - -# Represents a 'labeled' event on a given issue or pull request. -type LabeledEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the label associated with the 'labeled' event. - label: Label! - - # Identifies the `Labelable` associated with the event. - labelable: Labelable! -} - -# Represents a given language found in repositories. -type Language implements Node { - # The color defined for the current language. - color: String - id: ID! - - # The name of the current language. - name: String! -} - -# A list of languages associated with the parent. -type LanguageConnection { - # A list of edges. - edges: [LanguageEdge] - - # A list of nodes. - nodes: [Language] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! - - # The total size in bytes of files written in that language. - totalSize: Int! -} - -# Represents the language of a repository. -type LanguageEdge { - cursor: String! - node: Language! - - # The number of bytes of code written in the language. - size: Int! -} - -# Ordering options for language connections. -input LanguageOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order languages by. - field: LanguageOrderField! -} - -# Properties by which language connections can be ordered. -enum LanguageOrderField { - # Order languages by the size of all files containing the language - SIZE -} - -# A repository's open source license -type License implements Node { - # The full text of the license - body: String! - - # The conditions set by the license - conditions: [LicenseRule]! - - # A human-readable description of the license - description: String - - # Whether the license should be featured - featured: Boolean! - - # Whether the license should be displayed in license pickers - hidden: Boolean! - id: ID! - - # Instructions on how to implement the license - implementation: String - - # The lowercased SPDX ID of the license - key: String! - - # The limitations set by the license - limitations: [LicenseRule]! - - # The license full name specified by - name: String! - - # Customary short name if applicable (e.g, GPLv3) - nickname: String - - # The permissions set by the license - permissions: [LicenseRule]! - - # Whether the license is a pseudo-license placeholder (e.g., other, no-license) - pseudoLicense: Boolean! - - # Short identifier specified by - spdxId: String - - # URL to the license on - url: URI -} - -# Describes a License's conditions, permissions, and limitations -type LicenseRule { - # A description of the rule - description: String! - - # The machine-readable rule key - key: String! - - # The human-readable rule label - label: String! -} - -# Autogenerated input type of LockLockable -input LockLockableInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # A reason for why the issue or pull request will be locked. - lockReason: LockReason - - # ID of the issue or pull request to be locked. - lockableId: ID! -} - -# Autogenerated return type of LockLockable -type LockLockablePayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The item that was locked. - lockedRecord: Lockable -} - -# The possible reasons that an issue or pull request was locked. -enum LockReason { - # The issue or pull request was locked because the conversation was off-topic. - OFF_TOPIC - - # The issue or pull request was locked because the conversation was resolved. - RESOLVED - - # The issue or pull request was locked because the conversation was spam. - SPAM - - # The issue or pull request was locked because the conversation was too heated. - TOO_HEATED -} - -# An object that can be locked. -interface Lockable { - # Reason that the conversation was locked. - activeLockReason: LockReason - - # `true` if the object is locked - locked: Boolean! -} - -# Represents a 'locked' event on a given issue or pull request. -type LockedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Reason that the conversation was locked (optional). - lockReason: LockReason - - # Object that was locked. - lockable: Lockable! -} - -# A public description of a Marketplace category. -type MarketplaceCategory implements Node { - # The category's description. - description: String - - # The technical description of how apps listed in this category work with GitHub. - howItWorks: String - id: ID! - - # The category's name. - name: String! - - # How many Marketplace listings have this as their primary category. - primaryListingCount: Int! - - # The HTTP path for this Marketplace category. - resourcePath: URI! - - # How many Marketplace listings have this as their secondary category. - secondaryListingCount: Int! - - # The short name of the category used in its URL. - slug: String! - - # The HTTP URL for this Marketplace category. - url: URI! -} - -# An edge in a connection. -type MarketplaceCategoryEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: MarketplaceCategory -} - -# A listing in the GitHub integration marketplace. -type MarketplaceListing implements Node { - # The GitHub App this listing represents. - app: App - - # URL to the listing owner's company site. - companyUrl: URI - - # The HTTP path for configuring access to the listing's integration or OAuth app - configurationResourcePath: URI! - - # The HTTP URL for configuring access to the listing's integration or OAuth app - configurationUrl: URI! - - # URL to the listing's documentation. - documentationUrl: URI - - # The listing's detailed description. - extendedDescription: String - - # The listing's detailed description rendered to HTML. - extendedDescriptionHTML: HTML! - - # The listing's introductory description. - fullDescription: String! - - # The listing's introductory description rendered to HTML. - fullDescriptionHTML: HTML! - - # Whether this listing has been submitted for review from GitHub for approval to be displayed in the Marketplace. - hasApprovalBeenRequested: Boolean! - - # Does this listing have any plans with a free trial? - hasPublishedFreeTrialPlans: Boolean! - - # Does this listing have a terms of service link? - hasTermsOfService: Boolean! - - # A technical description of how this app works with GitHub. - howItWorks: String - - # The listing's technical description rendered to HTML. - howItWorksHTML: HTML! - id: ID! - - # URL to install the product to the viewer's account or organization. - installationUrl: URI - - # Whether this listing's app has been installed for the current viewer - installedForViewer: Boolean! - - # Whether this listing has been approved for display in the Marketplace. - isApproved: Boolean! - - # Whether this listing has been removed from the Marketplace. - isDelisted: Boolean! - - # Whether this listing is still an editable draft that has not been submitted - # for review and is not publicly visible in the Marketplace. - isDraft: Boolean! - - # Whether the product this listing represents is available as part of a paid plan. - isPaid: Boolean! - - # Whether this listing has been rejected by GitHub for display in the Marketplace. - isRejected: Boolean! - - # The hex color code, without the leading '#', for the logo background. - logoBackgroundColor: String! - - # URL for the listing's logo image. - logoUrl( - # The size in pixels of the resulting square image. - size: Int = 400 - ): URI - - # The listing's full name. - name: String! - - # The listing's very short description without a trailing period or ampersands. - normalizedShortDescription: String! - - # URL to the listing's detailed pricing. - pricingUrl: URI - - # The category that best describes the listing. - primaryCategory: MarketplaceCategory! - - # URL to the listing's privacy policy. - privacyPolicyUrl: URI! - - # The HTTP path for the Marketplace listing. - resourcePath: URI! - - # The URLs for the listing's screenshots. - screenshotUrls: [String]! - - # An alternate category that describes the listing. - secondaryCategory: MarketplaceCategory - - # The listing's very short description. - shortDescription: String! - - # The short name of the listing used in its URL. - slug: String! - - # URL to the listing's status page. - statusUrl: URI - - # An email address for support for this listing's app. - supportEmail: String - - # Either a URL or an email address for support for this listing's app. - supportUrl: URI! - - # URL to the listing's terms of service. - termsOfServiceUrl: URI - - # The HTTP URL for the Marketplace listing. - url: URI! - - # Can the current viewer add plans for this Marketplace listing. - viewerCanAddPlans: Boolean! - - # Can the current viewer approve this Marketplace listing. - viewerCanApprove: Boolean! - - # Can the current viewer delist this Marketplace listing. - viewerCanDelist: Boolean! - - # Can the current viewer edit this Marketplace listing. - viewerCanEdit: Boolean! - - # Can the current viewer edit the primary and secondary category of this - # Marketplace listing. - viewerCanEditCategories: Boolean! - - # Can the current viewer edit the plans for this Marketplace listing. - viewerCanEditPlans: Boolean! - - # Can the current viewer return this Marketplace listing to draft state - # so it becomes editable again. - viewerCanRedraft: Boolean! - - # Can the current viewer reject this Marketplace listing by returning it to - # an editable draft state or rejecting it entirely. - viewerCanReject: Boolean! - - # Can the current viewer request this listing be reviewed for display in - # the Marketplace. - viewerCanRequestApproval: Boolean! - - # Indicates whether the current user has an active subscription to this Marketplace listing. - viewerHasPurchased: Boolean! - - # Indicates if the current user has purchased a subscription to this Marketplace listing - # for all of the organizations the user owns. - viewerHasPurchasedForAllOrganizations: Boolean! - - # Does the current viewer role allow them to administer this Marketplace listing. - viewerIsListingAdmin: Boolean! -} - -# Look up Marketplace Listings -type MarketplaceListingConnection { - # A list of edges. - edges: [MarketplaceListingEdge] - - # A list of nodes. - nodes: [MarketplaceListing] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type MarketplaceListingEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: MarketplaceListing -} - -# Represents a 'mentioned' event on a given issue or pull request. -type MentionedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# Whether or not a PullRequest can be merged. -enum MergeableState { - # The pull request cannot be merged due to merge conflicts. - CONFLICTING - - # The pull request can be merged. - MERGEABLE - - # The mergeability of the pull request is still being calculated. - UNKNOWN -} - -# Represents a 'merged' event on a given pull request. -type MergedEvent implements Node & UniformResourceLocatable { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the commit associated with the `merge` event. - commit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the Ref associated with the `merge` event. - mergeRef: Ref - - # Identifies the name of the Ref associated with the `merge` event. - mergeRefName: String! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # The HTTP path for this merged event. - resourcePath: URI! - - # The HTTP URL for this merged event. - url: URI! -} - -# Represents a Milestone object on a given repository. -type Milestone implements Closable & Node & UniformResourceLocatable { - # `true` if the object is closed (definition of closed may depend on type) - closed: Boolean! - - # Identifies the date and time when the object was closed. - closedAt: DateTime - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the actor who created the milestone. - creator: Actor - - # Identifies the description of the milestone. - description: String - - # Identifies the due date of the milestone. - dueOn: DateTime - id: ID! - - # A list of issues associated with the milestone. - issues( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for issues returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the issues by. - states: [IssueState!] - ): IssueConnection! - - # Identifies the number of the milestone. - number: Int! - - # A list of pull requests associated with the milestone. - pullRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # The base ref name to filter the pull requests by. - baseRefName: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # The head ref name to filter the pull requests by. - headRefName: String - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for pull requests returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the pull requests by. - states: [PullRequestState!] - ): PullRequestConnection! - - # The repository associated with this milestone. - repository: Repository! - - # The HTTP path for this milestone - resourcePath: URI! - - # Identifies the state of the milestone. - state: MilestoneState! - - # Identifies the title of the milestone. - title: String! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this milestone - url: URI! -} - -# The connection type for Milestone. -type MilestoneConnection { - # A list of edges. - edges: [MilestoneEdge] - - # A list of nodes. - nodes: [Milestone] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type MilestoneEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Milestone -} - -# Types that can be inside a Milestone. -union MilestoneItem = Issue | PullRequest - -# Ordering options for milestone connections. -input MilestoneOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order milestones by. - field: MilestoneOrderField! -} - -# Properties by which milestone connections can be ordered. -enum MilestoneOrderField { - # Order milestones by when they were created. - CREATED_AT - - # Order milestones by when they are due. - DUE_DATE - - # Order milestones by their number. - NUMBER - - # Order milestones by when they were last updated. - UPDATED_AT -} - -# The possible states of a milestone. -enum MilestoneState { - # A milestone that has been closed. - CLOSED - - # A milestone that is still open. - OPEN -} - -# Represents a 'milestoned' event on a given issue or pull request. -type MilestonedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the milestone title associated with the 'milestoned' event. - milestoneTitle: String! - - # Object referenced by event. - subject: MilestoneItem! -} - -# Autogenerated input type of MoveProjectCard -input MoveProjectCardInput { - # Place the new card after the card with this id. Pass null to place it at the top. - afterCardId: ID - - # The id of the card to move. - cardId: ID! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The id of the column to move it into. - columnId: ID! -} - -# Autogenerated return type of MoveProjectCard -type MoveProjectCardPayload { - # The new edge of the moved card. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `cardEdge` will change from `ProjectCardEdge!` to `ProjectCardEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - cardEdge: ProjectCardEdge! - - # A unique identifier for the client performing the mutation. - clientMutationId: String -} - -# Autogenerated input type of MoveProjectColumn -input MoveProjectColumnInput { - # Place the new column after the column with this id. Pass null to place it at the front. - afterColumnId: ID - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The id of the column to move. - columnId: ID! -} - -# Autogenerated return type of MoveProjectColumn -type MoveProjectColumnPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The new edge of the moved column. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `columnEdge` will change from `ProjectColumnEdge!` to `ProjectColumnEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - columnEdge: ProjectColumnEdge! -} - -# Represents a 'moved_columns_in_project' event on a given issue or pull request. -type MovedColumnsInProjectEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# The root query for implementing GraphQL mutations. -type Mutation { - # Applies a suggested topic to the repository. - acceptTopicSuggestion(input: AcceptTopicSuggestionInput!): AcceptTopicSuggestionPayload - - # Adds a comment to an Issue or Pull Request. - addComment(input: AddCommentInput!): AddCommentPayload - - # Adds a card to a ProjectColumn. Either `contentId` or `note` must be provided but **not** both. - addProjectCard(input: AddProjectCardInput!): AddProjectCardPayload - - # Adds a column to a Project. - addProjectColumn(input: AddProjectColumnInput!): AddProjectColumnPayload - - # Adds a review to a Pull Request. - addPullRequestReview(input: AddPullRequestReviewInput!): AddPullRequestReviewPayload - - # Adds a comment to a review. - addPullRequestReviewComment(input: AddPullRequestReviewCommentInput!): AddPullRequestReviewCommentPayload - - # Adds a reaction to a subject. - addReaction(input: AddReactionInput!): AddReactionPayload - - # Adds a star to a Starrable. - addStar(input: AddStarInput!): AddStarPayload - - # Creates a new project. - createProject(input: CreateProjectInput!): CreateProjectPayload - - # Rejects a suggested topic for the repository. - declineTopicSuggestion(input: DeclineTopicSuggestionInput!): DeclineTopicSuggestionPayload - - # Deletes a project. - deleteProject(input: DeleteProjectInput!): DeleteProjectPayload - - # Deletes a project card. - deleteProjectCard(input: DeleteProjectCardInput!): DeleteProjectCardPayload - - # Deletes a project column. - deleteProjectColumn(input: DeleteProjectColumnInput!): DeleteProjectColumnPayload - - # Deletes a pull request review. - deletePullRequestReview(input: DeletePullRequestReviewInput!): DeletePullRequestReviewPayload - - # Dismisses an approved or rejected pull request review. - dismissPullRequestReview(input: DismissPullRequestReviewInput!): DismissPullRequestReviewPayload - - # Lock a lockable object - lockLockable(input: LockLockableInput!): LockLockablePayload - - # Moves a project card to another place. - moveProjectCard(input: MoveProjectCardInput!): MoveProjectCardPayload - - # Moves a project column to another place. - moveProjectColumn(input: MoveProjectColumnInput!): MoveProjectColumnPayload - - # Removes outside collaborator from all repositories in an organization. - removeOutsideCollaborator(input: RemoveOutsideCollaboratorInput!): RemoveOutsideCollaboratorPayload - - # Removes a reaction from a subject. - removeReaction(input: RemoveReactionInput!): RemoveReactionPayload - - # Removes a star from a Starrable. - removeStar(input: RemoveStarInput!): RemoveStarPayload - - # Set review requests on a pull request. - requestReviews(input: RequestReviewsInput!): RequestReviewsPayload - - # Submits a pending pull request review. - submitPullRequestReview(input: SubmitPullRequestReviewInput!): SubmitPullRequestReviewPayload - - # Unlock a lockable object - unlockLockable(input: UnlockLockableInput!): UnlockLockablePayload - - # Updates an existing project. - updateProject(input: UpdateProjectInput!): UpdateProjectPayload - - # Updates an existing project card. - updateProjectCard(input: UpdateProjectCardInput!): UpdateProjectCardPayload - - # Updates an existing project column. - updateProjectColumn(input: UpdateProjectColumnInput!): UpdateProjectColumnPayload - - # Updates the body of a pull request review. - updatePullRequestReview(input: UpdatePullRequestReviewInput!): UpdatePullRequestReviewPayload - - # Updates a pull request review comment. - updatePullRequestReviewComment(input: UpdatePullRequestReviewCommentInput!): UpdatePullRequestReviewCommentPayload - - # Updates the state for subscribable subjects. - updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionPayload - - # Replaces the repository's topics with the given topics. - updateTopics(input: UpdateTopicsInput!): UpdateTopicsPayload -} - -# An object with an ID. -interface Node { - # ID of the object. - id: ID! -} - -# Possible directions in which to order a list of items when provided an `orderBy` argument. -enum OrderDirection { - # Specifies an ascending order for a given `orderBy` argument. - ASC - - # Specifies a descending order for a given `orderBy` argument. - DESC -} - -# An account on GitHub, with one or more owners, that has repositories, members and teams. -type Organization implements Actor & Node & ProjectOwner & RegistryPackageOwner & RegistryPackageSearch & RepositoryOwner & UniformResourceLocatable { - # A URL pointing to the organization's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - - # Identifies the primary key from the database. - databaseId: Int - - # The organization's public profile description. - description: String - - # The organization's public email. - email: String - id: ID! - - # Whether the organization has verified its profile email and website. - isVerified: Boolean! - - # The organization's public profile location. - location: String - - # The organization's login name. - login: String! - - # A list of users who are members of this organization. - members( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # The organization's public profile name. - name: String - - # The HTTP path creating a new team - newTeamResourcePath: URI! - - # The HTTP URL creating a new team - newTeamUrl: URI! - - # The billing email for the organization. - organizationBillingEmail: String - - # A list of repositories this user has pinned to their profile - pinnedRepositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # Find project by number. - project( - # The project number to find. - number: Int! - ): Project - - # A list of projects under the owner. - projects( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for projects returned from the connection - orderBy: ProjectOrder - - # Query to search projects by, currently only searching by name. - search: String - - # A list of states to filter the projects by. - states: [ProjectState!] - ): ProjectConnection! - - # The HTTP path listing organization's projects - projectsResourcePath: URI! - - # The HTTP URL listing organization's projects - projectsUrl: URI! - - # A list of repositories that the user owns. - repositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they are forks of another repository - isFork: Boolean - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # Find Repository. - repository( - # Name of Repository to find. - name: String! - ): Repository - - # When true the organization requires all members, billing managers, and outside - # collaborators to enable two-factor authentication. - requiresTwoFactorAuthentication: Boolean - - # The HTTP path for this organization. - resourcePath: URI! - - # The Organization's SAML Identity Providers - samlIdentityProvider: OrganizationIdentityProvider - - # Find an organization's team by its slug. - team( - # The name or slug of the team to find. - slug: String! - ): Team - - # A list of teams in this organization. - teams( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # If true, filters teams that are mapped to an LDAP Group (Enterprise only) - ldapMapped: Boolean - - # Ordering options for teams returned from the connection - orderBy: TeamOrder - - # If non-null, filters teams according to privacy - privacy: TeamPrivacy - - # If non-null, filters teams with query on team name and team slug - query: String - - # If non-null, filters teams according to whether the viewer is an admin or member on team - role: TeamRole - - # If true, restrict to only root teams - rootTeamsOnly: Boolean = false - - # User logins to filter by - userLogins: [String!] - ): TeamConnection! - - # The HTTP path listing organization's teams - teamsResourcePath: URI! - - # The HTTP URL listing organization's teams - teamsUrl: URI! - - # The HTTP URL for this organization. - url: URI! - - # Organization is adminable by the viewer. - viewerCanAdminister: Boolean! - - # Can the current viewer create new projects on this owner. - viewerCanCreateProjects: Boolean! - - # Viewer can create repositories on this organization - viewerCanCreateRepositories: Boolean! - - # Viewer can create teams on this organization. - viewerCanCreateTeams: Boolean! - - # Viewer is an active member of this organization. - viewerIsAMember: Boolean! - - # The organization's public profile URL. - websiteUrl: URI -} - -# The connection type for Organization. -type OrganizationConnection { - # A list of edges. - edges: [OrganizationEdge] - - # A list of nodes. - nodes: [Organization] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type OrganizationEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Organization -} - -# An Identity Provider configured to provision SAML and SCIM identities for Organizations -type OrganizationIdentityProvider implements Node { - # The digest algorithm used to sign SAML requests for the Identity Provider. - digestMethod: URI - - # External Identities provisioned by this Identity Provider - externalIdentities( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ExternalIdentityConnection! - id: ID! - - # The x509 certificate used by the Identity Provder to sign assertions and responses. - idpCertificate: X509Certificate - - # The Issuer Entity ID for the SAML Identity Provider - issuer: String - - # Organization this Identity Provider belongs to - organization: Organization - - # The signature algorithm used to sign SAML requests for the Identity Provider. - signatureMethod: URI - - # The URL endpoint for the Identity Provider's SAML SSO. - ssoUrl: URI -} - -# An Invitation for a user to an organization. -type OrganizationInvitation implements Node { - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The email address of the user invited to the organization. - email: String - id: ID! - - # The type of invitation that was sent (e.g. email, user). - invitationType: OrganizationInvitationType! - - # The user who was invited to the organization. - invitee: User - - # The user who created the invitation. - inviter: User! - - # The organization the invite is for - organization: Organization! - - # The user's pending role in the organization (e.g. member, owner). - role: OrganizationInvitationRole! -} - -# The connection type for OrganizationInvitation. -type OrganizationInvitationConnection { - # A list of edges. - edges: [OrganizationInvitationEdge] - - # A list of nodes. - nodes: [OrganizationInvitation] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type OrganizationInvitationEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: OrganizationInvitation -} - -# The possible organization invitation roles. -enum OrganizationInvitationRole { - # The user is invited to be an admin of the organization. - ADMIN - - # The user is invited to be a billing manager of the organization. - BILLING_MANAGER - - # The user is invited to be a direct member of the organization. - DIRECT_MEMBER - - # The user's previous role will be reinstated. - REINSTATE -} - -# The possible organization invitation types. -enum OrganizationInvitationType { - # The invitation was to an email address. - EMAIL - - # The invitation was to an existing user. - USER -} - -# Information about pagination in a connection. -type PageInfo { - # When paginating forwards, the cursor to continue. - endCursor: String - - # When paginating forwards, are there more items? - hasNextPage: Boolean! - - # When paginating backwards, are there more items? - hasPreviousPage: Boolean! - - # When paginating backwards, the cursor to continue. - startCursor: String -} - -# Projects manage issues, pull requests and notes within a project owner. -type Project implements Closable & Node & Updatable { - # The project's description body. - body: String - - # The projects description body rendered to HTML. - bodyHTML: HTML! - - # `true` if the object is closed (definition of closed may depend on type) - closed: Boolean! - - # Identifies the date and time when the object was closed. - closedAt: DateTime - - # List of columns in the project - columns( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProjectColumnConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The actor who originally created the project. - creator: Actor - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # The project's name. - name: String! - - # The project's number. - number: Int! - - # The project's owner. Currently limited to repositories and organizations. - owner: ProjectOwner! - - # List of pending cards in this project - pendingCards( - # Returns the elements in the list that come after the specified cursor. - after: String - - # A list of archived states to filter the cards by - archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProjectCardConnection! - - # The HTTP path for this project - resourcePath: URI! - - # Whether the project is open or closed. - state: ProjectState! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this project - url: URI! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! -} - -# A card in a project. -type ProjectCard implements Node { - # The project column this card is associated under. A card may only belong to one - # project column at a time. The column field will be null if the card is created - # in a pending state and has yet to be associated with a column. Once cards are - # associated with a column, they will not become pending in the future. - column: ProjectColumn - - # The card content item - content: ProjectCardItem - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The actor who created this card - creator: Actor - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # Whether the card is archived - isArchived: Boolean! - - # The card note - note: String - - # The project that contains this card. - project: Project! - - # The HTTP path for this card - resourcePath: URI! - - # The state of ProjectCard - state: ProjectCardState - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this card - url: URI! -} - -# The possible archived states of a project card. -enum ProjectCardArchivedState { - # A project card that is archived - ARCHIVED - - # A project card that is not archived - NOT_ARCHIVED -} - -# The connection type for ProjectCard. -type ProjectCardConnection { - # A list of edges. - edges: [ProjectCardEdge] - - # A list of nodes. - nodes: [ProjectCard] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ProjectCardEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ProjectCard -} - -# Types that can be inside Project Cards. -union ProjectCardItem = Issue | PullRequest - -# Various content states of a ProjectCard -enum ProjectCardState { - # The card has content only. - CONTENT_ONLY - - # The card has a note only. - NOTE_ONLY - - # The card is redacted. - REDACTED -} - -# A column inside a project. -type ProjectColumn implements Node { - # List of cards in the column - cards( - # Returns the elements in the list that come after the specified cursor. - after: String - - # A list of archived states to filter the cards by - archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProjectCardConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # The project column's name. - name: String! - - # The project that contains this column. - project: Project! - - # The semantic purpose of the column - purpose: ProjectColumnPurpose - - # The HTTP path for this project column - resourcePath: URI! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this project column - url: URI! -} - -# The connection type for ProjectColumn. -type ProjectColumnConnection { - # A list of edges. - edges: [ProjectColumnEdge] - - # A list of nodes. - nodes: [ProjectColumn] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ProjectColumnEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ProjectColumn -} - -# The semantic purpose of the column - todo, in progress, or done. -enum ProjectColumnPurpose { - # The column contains cards which are complete - DONE - - # The column contains cards which are currently being worked on - IN_PROGRESS - - # The column contains cards still to be worked on - TODO -} - -# A list of projects associated with the owner. -type ProjectConnection { - # A list of edges. - edges: [ProjectEdge] - - # A list of nodes. - nodes: [Project] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ProjectEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Project -} - -# Ways in which lists of projects can be ordered upon return. -input ProjectOrder { - # The direction in which to order projects by the specified field. - direction: OrderDirection! - - # The field in which to order projects by. - field: ProjectOrderField! -} - -# Properties by which project connections can be ordered. -enum ProjectOrderField { - # Order projects by creation time - CREATED_AT - - # Order projects by name - NAME - - # Order projects by update time - UPDATED_AT -} - -# Represents an owner of a Project. -interface ProjectOwner { - id: ID! - - # Find project by number. - project( - # The project number to find. - number: Int! - ): Project - - # A list of projects under the owner. - projects( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for projects returned from the connection - orderBy: ProjectOrder - - # Query to search projects by, currently only searching by name. - search: String - - # A list of states to filter the projects by. - states: [ProjectState!] - ): ProjectConnection! - - # The HTTP path listing owners projects - projectsResourcePath: URI! - - # The HTTP URL listing owners projects - projectsUrl: URI! - - # Can the current viewer create new projects on this owner. - viewerCanCreateProjects: Boolean! -} - -# State of the project; either 'open' or 'closed' -enum ProjectState { - # The project is closed. - CLOSED - - # The project is open. - OPEN -} - -# A repository protected branch. -type ProtectedBranch implements Node { - # The actor who created this protected branch. - creator: Actor - - # Will new commits pushed to this branch dismiss pull request review approvals. - hasDismissableStaleReviews: Boolean! - - # Are reviews required to update this branch. - hasRequiredReviews: Boolean! - - # Are status checks required to update this branch. - hasRequiredStatusChecks: Boolean! - - # Is pushing to this branch restricted. - hasRestrictedPushes: Boolean! - - # Is dismissal of pull request reviews restricted. - hasRestrictedReviewDismissals: Boolean! - - # Are branches required to be up to date before merging. - hasStrictRequiredStatusChecks: Boolean! - id: ID! - - # Can admins overwrite branch protection. - isAdminEnforced: Boolean! - - # The name of the protected branch rule. - name: String! - - # A list push allowances for this protected branch. - pushAllowances( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): PushAllowanceConnection! - - # The repository associated with this protected branch. - repository: Repository! - - # List of required status check contexts that must pass for commits to be accepted to this branch. - requiredStatusCheckContexts: [String] - - # A list review dismissal allowances for this protected branch. - reviewDismissalAllowances( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ReviewDismissalAllowanceConnection! -} - -# The connection type for ProtectedBranch. -type ProtectedBranchConnection { - # A list of edges. - edges: [ProtectedBranchEdge] - - # A list of nodes. - nodes: [ProtectedBranch] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ProtectedBranchEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ProtectedBranch -} - -# A user's public key. -type PublicKey implements Node { - id: ID! - - # The public key string - key: String! -} - -# The connection type for PublicKey. -type PublicKeyConnection { - # A list of edges. - edges: [PublicKeyEdge] - - # A list of nodes. - nodes: [PublicKey] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PublicKeyEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PublicKey -} - -# A repository pull request. -type PullRequest implements Assignable & Closable & Comment & Labelable & Lockable & Node & Reactable & RepositoryNode & Subscribable & UniformResourceLocatable & Updatable & UpdatableComment { - # Reason that the conversation was locked. - activeLockReason: LockReason - - # The number of additions in this pull request. - additions: Int! - - # A list of Users assigned to this object. - assignees( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # Identifies the base Ref associated with the pull request. - baseRef: Ref - - # Identifies the name of the base Ref associated with the pull request, even if the ref has been deleted. - baseRefName: String! - - # Identifies the oid of the base ref associated with the pull request, even if the ref has been deleted. - baseRefOid: GitObjectID! - - # The body as Markdown. - body: String! - - # The body rendered to HTML. - bodyHTML: HTML! - - # The body rendered to text. - bodyText: String! - - # The number of changed files in this pull request. - changedFiles: Int! - - # `true` if the pull request is closed - closed: Boolean! - - # Identifies the date and time when the object was closed. - closedAt: DateTime - - # A list of comments associated with the pull request. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): IssueCommentConnection! - - # A list of commits present in this pull request's head branch not present in the base branch. - commits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): PullRequestCommitConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The number of deletions in this pull request. - deletions: Int! - - # The actor who edited this pull request's body. - editor: Actor - - # Identifies the head Ref associated with the pull request. - headRef: Ref - - # Identifies the name of the head Ref associated with the pull request, even if the ref has been deleted. - headRefName: String! - - # Identifies the oid of the head ref associated with the pull request, even if the ref has been deleted. - headRefOid: GitObjectID! - - # The repository associated with this pull request's head Ref. - headRepository: Repository - - # The owner of the repository associated with this pull request's head Ref. - headRepositoryOwner: RepositoryOwner - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The head and base repositories are different. - isCrossRepository: Boolean! - - # A list of labels associated with the object. - labels( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): LabelConnection - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # `true` if the pull request is locked - locked: Boolean! - - # Indicates whether maintainers can modify the pull request. - maintainerCanModify: Boolean! - - # The commit that was created when this pull request was merged. - mergeCommit: Commit - - # Whether or not the pull request can be merged based on the existence of merge conflicts. - mergeable: MergeableState! - - # Whether or not the pull request was merged. - merged: Boolean! - - # The date and time that the pull request was merged. - mergedAt: DateTime - - # The actor who merged the pull request. - mergedBy: Actor - - # Identifies the milestone associated with the pull request. - milestone: Milestone - - # Identifies the pull request number. - number: Int! - - # A list of Users that are participating in the Pull Request conversation. - participants( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # The permalink to the pull request. - permalink: URI! - - # The commit that GitHub automatically generated to test if this pull request - # could be merged. This field will not return a value if the pull request is - # merged, or if the test merge commit is still being generated. See the - # `mergeable` field for more details on the mergeability of the pull request. - potentialMergeCommit: Commit - - # List of project cards associated with this pull request. - projectCards( - # Returns the elements in the list that come after the specified cursor. - after: String - - # A list of archived states to filter the cards by - archivedStates: [ProjectCardArchivedState] = [ARCHIVED, NOT_ARCHIVED] - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProjectCardConnection! - - # Identifies when the comment was published at. - publishedAt: DateTime - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # The repository associated with this node. - repository: Repository! - - # The HTTP path for this pull request. - resourcePath: URI! - - # The HTTP path for reverting this pull request. - revertResourcePath: URI! - - # The HTTP URL for reverting this pull request. - revertUrl: URI! - - # A list of review requests associated with the pull request. - reviewRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ReviewRequestConnection - - # A list of reviews associated with the pull request. - reviews( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Filter by author of the review. - author: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # A list of states to filter the reviews. - states: [PullRequestReviewState!] - ): PullRequestReviewConnection - - # Identifies the state of the pull request. - state: PullRequestState! - - # A list of reviewer suggestions based on commit history and past review comments. - suggestedReviewers: [SuggestedReviewer]! - - # A list of events, comments, commits, etc. associated with the pull request. - timeline( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows filtering timeline events by a `since` timestamp. - since: DateTime - ): PullRequestTimelineConnection! - - # Identifies the pull request title. - title: String! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this pull request. - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Whether or not the viewer can apply suggestion. - viewerCanApplySuggestion: Boolean! - - # Can user react to this subject - viewerCanReact: Boolean! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState -} - -# Represents a Git commit part of a pull request. -type PullRequestCommit implements Node & UniformResourceLocatable { - # The Git commit object - commit: Commit! - id: ID! - - # The pull request this commit belongs to - pullRequest: PullRequest! - - # The HTTP path for this pull request commit - resourcePath: URI! - - # The HTTP URL for this pull request commit - url: URI! -} - -# The connection type for PullRequestCommit. -type PullRequestCommitConnection { - # A list of edges. - edges: [PullRequestCommitEdge] - - # A list of nodes. - nodes: [PullRequestCommit] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PullRequestCommitEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequestCommit -} - -# The connection type for PullRequest. -type PullRequestConnection { - # A list of edges. - edges: [PullRequestEdge] - - # A list of nodes. - nodes: [PullRequest] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PullRequestEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequest -} - -# Ways in which lists of issues can be ordered upon return. -input PullRequestOrder { - # The direction in which to order pull requests by the specified field. - direction: OrderDirection! - - # The field in which to order pull requests by. - field: PullRequestOrderField! -} - -# Properties by which pull_requests connections can be ordered. -enum PullRequestOrderField { - # Order pull_requests by creation time - CREATED_AT - - # Order pull_requests by update time - UPDATED_AT -} - -# The possible PubSub channels for a pull request. -enum PullRequestPubSubTopic { - # The channel ID for observing head ref updates. - HEAD_REF - - # The channel ID for marking an pull request as read. - MARKASREAD - - # The channel ID for observing pull request state updates. - STATE - - # The channel ID for updating items on the pull request timeline. - TIMELINE - - # The channel ID for observing pull request updates. - UPDATED -} - -# A review object for a given pull request. -type PullRequestReview implements Comment & Deletable & Node & RepositoryNode & Updatable & UpdatableComment { - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # Identifies the pull request review body. - body: String! - - # The body of this review rendered to HTML. - bodyHTML: HTML! - - # The body of this review rendered as plain text. - bodyText: String! - - # A list of review comments for the current pull request review. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): PullRequestReviewCommentConnection! - - # Identifies the commit associated with this pull request review. - commit: Commit - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies when the comment was published at. - publishedAt: DateTime - - # Identifies the pull request associated with this pull request review. - pullRequest: PullRequest! - - # The repository associated with this node. - repository: Repository! - - # The HTTP path permalink for this PullRequestReview. - resourcePath: URI! - - # Identifies the current state of the pull request review. - state: PullRequestReviewState! - - # Identifies when the Pull Request Review was submitted - submittedAt: DateTime - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL permalink for this PullRequestReview. - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# A review comment associated with a given repository pull request. -type PullRequestReviewComment implements Comment & Deletable & Node & Reactable & RepositoryNode & Updatable & UpdatableComment { - # The actor who authored the comment. - author: Actor - - # Author's association with the subject of the comment. - authorAssociation: CommentAuthorAssociation! - - # The comment body of this review comment. - body: String! - - # The comment body of this review comment rendered to HTML. - bodyHTML: HTML! - - # The comment body of this review comment rendered as plain text. - bodyText: String! - - # Identifies the commit associated with the comment. - commit: Commit! - - # Identifies when the comment was created. - createdAt: DateTime! - - # Check if this comment was created via an email reply. - createdViaEmail: Boolean! - - # Identifies the primary key from the database. - databaseId: Int - - # The diff hunk to which the comment applies. - diffHunk: String! - - # Identifies when the comment was created in a draft state. - draftedAt: DateTime! - - # The actor who edited the comment. - editor: Actor - id: ID! - - # Check if this comment was edited and includes an edit with the creation data - includesCreatedEdit: Boolean! - - # The moment the editor made the last edit - lastEditedAt: DateTime - - # Identifies the original commit associated with the comment. - originalCommit: Commit - - # The original line index in the diff to which the comment applies. - originalPosition: Int! - - # The path to which the comment applies. - path: String! - - # The line index in the diff to which the comment applies. - position: Int - - # Identifies when the comment was published at. - publishedAt: DateTime - - # The pull request associated with this review comment. - pullRequest: PullRequest! - - # The pull request review associated with this review comment. - pullRequestReview: PullRequestReview - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # The comment this is a reply to. - replyTo: PullRequestReviewComment - - # The repository associated with this node. - repository: Repository! - - # The HTTP path permalink for this review comment. - resourcePath: URI! - - # Identifies when the comment was last updated. - updatedAt: DateTime! - - # The HTTP URL permalink for this review comment. - url: URI! - - # A list of edits to this content. - userContentEdits( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserContentEditConnection - - # Check if the current viewer can delete this object. - viewerCanDelete: Boolean! - - # Can user react to this subject - viewerCanReact: Boolean! - - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! - - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! - - # Did the viewer author this comment. - viewerDidAuthor: Boolean! -} - -# The connection type for PullRequestReviewComment. -type PullRequestReviewCommentConnection { - # A list of edges. - edges: [PullRequestReviewCommentEdge] - - # A list of nodes. - nodes: [PullRequestReviewComment] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PullRequestReviewCommentEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequestReviewComment -} - -# The connection type for PullRequestReview. -type PullRequestReviewConnection { - # A list of edges. - edges: [PullRequestReviewEdge] - - # A list of nodes. - nodes: [PullRequestReview] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PullRequestReviewEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequestReview -} - -# The possible events to perform on a pull request review. -enum PullRequestReviewEvent { - # Submit feedback and approve merging these changes. - APPROVE - - # Submit general feedback without explicit approval. - COMMENT - - # Dismiss review so it now longer effects merging. - DISMISS - - # Submit feedback that must be addressed before merging. - REQUEST_CHANGES -} - -# The possible states of a pull request review. -enum PullRequestReviewState { - # A review allowing the pull request to merge. - APPROVED - - # A review blocking the pull request from merging. - CHANGES_REQUESTED - - # An informational review. - COMMENTED - - # A review that has been dismissed. - DISMISSED - - # A review that has not yet been submitted. - PENDING -} - -# A threaded list of comments for a given pull request. -type PullRequestReviewThread implements Node { - # A list of pull request comments associated with the thread. - comments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): PullRequestReviewCommentConnection! - id: ID! - - # Identifies the pull request associated with this thread. - pullRequest: PullRequest! - - # Identifies the repository associated with this thread. - repository: Repository! -} - -# The possible states of a pull request. -enum PullRequestState { - # A pull request that has been closed without being merged. - CLOSED - - # A pull request that has been closed by being merged. - MERGED - - # A pull request that is still open. - OPEN -} - -# The connection type for PullRequestTimelineItem. -type PullRequestTimelineConnection { - # A list of edges. - edges: [PullRequestTimelineItemEdge] - - # A list of nodes. - nodes: [PullRequestTimelineItem] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An item in an pull request timeline -union PullRequestTimelineItem = AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit | CommitCommentThread | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MergedEvent | MilestonedEvent | PullRequestReview | PullRequestReviewComment | PullRequestReviewThread | ReferencedEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent - -# An edge in a connection. -type PullRequestTimelineItemEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequestTimelineItem -} - -# An item in a pull request timeline -union PullRequestTimelineItems = AddedToProjectEvent | AssignedEvent | BaseRefChangedEvent | BaseRefForcePushedEvent | ClosedEvent | CommentDeletedEvent | ConvertedNoteToIssueEvent | CrossReferencedEvent | DemilestonedEvent | DeployedEvent | DeploymentEnvironmentChangedEvent | HeadRefDeletedEvent | HeadRefForcePushedEvent | HeadRefRestoredEvent | IssueComment | LabeledEvent | LockedEvent | MentionedEvent | MergedEvent | MilestonedEvent | MovedColumnsInProjectEvent | PullRequestCommit | PullRequestReview | PullRequestReviewThread | ReferencedEvent | RemovedFromProjectEvent | RenamedTitleEvent | ReopenedEvent | ReviewDismissedEvent | ReviewRequestRemovedEvent | ReviewRequestedEvent | SubscribedEvent | UnassignedEvent | UnlabeledEvent | UnlockedEvent | UnsubscribedEvent - -# An edge in a connection. -type PullRequestTimelineItemsEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PullRequestTimelineItems -} - -# The possible item types found in a timeline. -enum PullRequestTimelineItemsItemType { - # Represents a 'added_to_project' event on a given issue or pull request. - ADDED_TO_PROJECT_EVENT - - # Represents an 'assigned' event on any assignable object. - ASSIGNED_EVENT - - # Represents a 'base_ref_changed' event on a given issue or pull request. - BASE_REF_CHANGED_EVENT - - # Represents a 'base_ref_force_pushed' event on a given pull request. - BASE_REF_FORCE_PUSHED_EVENT - - # Represents a 'closed' event on any `Closable`. - CLOSED_EVENT - - # Represents a 'comment_deleted' event on a given issue or pull request. - COMMENT_DELETED_EVENT - - # Represents a 'converted_note_to_issue' event on a given issue or pull request. - CONVERTED_NOTE_TO_ISSUE_EVENT - - # Represents a mention made by one issue or pull request to another. - CROSS_REFERENCED_EVENT - - # Represents a 'demilestoned' event on a given issue or pull request. - DEMILESTONED_EVENT - - # Represents a 'deployed' event on a given pull request. - DEPLOYED_EVENT - - # Represents a 'deployment_environment_changed' event on a given pull request. - DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT - - # Represents a 'head_ref_deleted' event on a given pull request. - HEAD_REF_DELETED_EVENT - - # Represents a 'head_ref_force_pushed' event on a given pull request. - HEAD_REF_FORCE_PUSHED_EVENT - - # Represents a 'head_ref_restored' event on a given pull request. - HEAD_REF_RESTORED_EVENT - - # Represents a comment on an Issue. - ISSUE_COMMENT - - # Represents a 'labeled' event on a given issue or pull request. - LABELED_EVENT - - # Represents a 'locked' event on a given issue or pull request. - LOCKED_EVENT - - # Represents a 'mentioned' event on a given issue or pull request. - MENTIONED_EVENT - - # Represents a 'merged' event on a given pull request. - MERGED_EVENT - - # Represents a 'milestoned' event on a given issue or pull request. - MILESTONED_EVENT - - # Represents a 'moved_columns_in_project' event on a given issue or pull request. - MOVED_COLUMNS_IN_PROJECT_EVENT - - # Represents a Git commit part of a pull request. - PULL_REQUEST_COMMIT - - # Represents a commit comment thread part of a pull request. - PULL_REQUEST_COMMIT_COMMENT_THREAD - - # A review object for a given pull request. - PULL_REQUEST_REVIEW - - # A threaded list of comments for a given pull request. - PULL_REQUEST_REVIEW_THREAD - - # Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. - PULL_REQUEST_REVISION_MARKER - - # Represents a 'referenced' event on a given `ReferencedSubject`. - REFERENCED_EVENT - - # Represents a 'removed_from_project' event on a given issue or pull request. - REMOVED_FROM_PROJECT_EVENT - - # Represents a 'renamed' event on a given issue or pull request - RENAMED_TITLE_EVENT - - # Represents a 'reopened' event on any `Closable`. - REOPENED_EVENT - - # Represents a 'review_dismissed' event on a given issue or pull request. - REVIEW_DISMISSED_EVENT - - # Represents an 'review_requested' event on a given pull request. - REVIEW_REQUESTED_EVENT - - # Represents an 'review_request_removed' event on a given pull request. - REVIEW_REQUEST_REMOVED_EVENT - - # Represents a 'subscribed' event on a given `Subscribable`. - SUBSCRIBED_EVENT - - # Represents an 'unassigned' event on any assignable object. - UNASSIGNED_EVENT - - # Represents an 'unlabeled' event on a given issue or pull request. - UNLABELED_EVENT - - # Represents an 'unlocked' event on a given issue or pull request. - UNLOCKED_EVENT - - # Represents an 'unsubscribed' event on a given `Subscribable`. - UNSUBSCRIBED_EVENT -} - -# A team or user who has the ability to push to a protected branch. -type PushAllowance implements Node { - # The actor that can push. - actor: PushAllowanceActor - id: ID! - - # Identifies the protected branch associated with the allowed user or team. - protectedBranch: ProtectedBranch! -} - -# Types that can be an actor. -union PushAllowanceActor = Team | User - -# The connection type for PushAllowance. -type PushAllowanceConnection { - # A list of edges. - edges: [PushAllowanceEdge] - - # A list of nodes. - nodes: [PushAllowance] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type PushAllowanceEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: PushAllowance -} - -# The query root of GitHub's GraphQL interface. -type Query { - # Look up a code of conduct by its key - codeOfConduct( - # The code of conduct's key - key: String! - ): CodeOfConduct - - # Look up a code of conduct by its key - codesOfConduct: [CodeOfConduct] - - # Look up an open source license by its key - license( - # The license's downcased SPDX ID - key: String! - ): License - - # Return a list of known open source licenses - licenses: [License]! - - # Get alphabetically sorted list of Marketplace categories - marketplaceCategories( - # Exclude categories with no listings. - excludeEmpty: Boolean - - # Exclude subcategories - excludeSubcategories: Boolean - - # Return only the specified categories. - includeCategories: [String!] - ): [MarketplaceCategory!]! - - # Look up a Marketplace category by its slug. - marketplaceCategory( - # The URL slug of the category. - slug: String! - - # Also check topic aliases for the category slug - useTopicAliases: Boolean - ): MarketplaceCategory - - # Look up a single Marketplace listing - marketplaceListing( - # Select the listing that matches this slug. It's the short name of the listing used in its URL. - slug: String! - ): MarketplaceListing - - # Look up Marketplace listings - marketplaceListings( - # Select listings that can be administered by the specified user. - adminId: ID - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Select listings visible to the viewer even if they are not approved. If omitted or - # false, only approved listings will be returned. - allStates: Boolean - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Select only listings with the given category. - categorySlug: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Select listings for products owned by the specified organization. - organizationId: ID - - # Select only listings where the primary category matches the given category slug. - primaryCategoryOnly: Boolean = false - - # Select the listings with these slugs, if they are visible to the viewer. - slugs: [String] - - # Also check topic aliases for the category slug - useTopicAliases: Boolean - - # Select listings to which user has admin access. If omitted, listings visible to the - # viewer are returned. - viewerCanAdmin: Boolean - - # Select only listings that offer a free trial. - withFreeTrialsOnly: Boolean = false - ): MarketplaceListingConnection! - - # Return information about the GitHub instance - meta: GitHubMetadata! - - # Fetches an object given its ID. - node( - # ID of the object. - id: ID! - ): Node - - # Lookup nodes by a list of IDs. - nodes( - # The list of node IDs. - ids: [ID!]! - ): [Node]! - - # Lookup a organization by login. - organization( - # The organization's login. - login: String! - ): Organization - - # The client's rate limit information. - rateLimit( - # If true, calculate the cost for the query without evaluating it - dryRun: Boolean = false - ): RateLimit - - # Hack to workaround https://github.com/facebook/relay/issues/112 re-exposing the root query object - relay: Query! - - # Lookup a given repository by the owner and repository name. - repository( - # The name of the repository - name: String! - - # The login field of a user or organization - owner: String! - ): Repository - - # Lookup a repository owner (ie. either a User or an Organization) by login. - repositoryOwner( - # The username to lookup the owner by. - login: String! - ): RepositoryOwner - - # Lookup resource by a URL. - resource( - # The URL. - url: URI! - ): UniformResourceLocatable - - # Perform a search across resources. - search( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # The search string to look for. - query: String! - - # The types of search items to search within. - type: SearchType! - ): SearchResultItemConnection! - - # Look up a topic by name. - topic( - # The topic's name. - name: String! - ): Topic - - # Lookup a user by login. - user( - # The user's login. - login: String! - ): User - - # The currently authenticated user. - viewer: User! -} - -# Represents the client's rate limit. -type RateLimit { - # The point cost for the current query counting against the rate limit. - cost: Int! - - # The maximum number of points the client is permitted to consume in a 60 minute window. - limit: Int! - - # The maximum number of nodes this query may return - nodeCount: Int! - - # The number of points remaining in the current rate limit window. - remaining: Int! - - # The time at which the current rate limit window resets in UTC epoch seconds. - resetAt: DateTime! -} - -# Represents a subject that can be reacted on. -interface Reactable { - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # A list of reactions grouped by content left on the subject. - reactionGroups: [ReactionGroup!] - - # A list of Reactions left on the Issue. - reactions( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Allows filtering Reactions by emoji. - content: ReactionContent - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Allows specifying the order in which reactions are returned. - orderBy: ReactionOrder - ): ReactionConnection! - - # Can user react to this subject - viewerCanReact: Boolean! -} - -# The connection type for User. -type ReactingUserConnection { - # A list of edges. - edges: [ReactingUserEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a user that's made a reaction. -type ReactingUserEdge { - # A cursor for use in pagination. - cursor: String! - node: User! - - # The moment when the user made the reaction. - reactedAt: DateTime! -} - -# An emoji reaction to a particular piece of content. -type Reaction implements Node { - # Identifies the emoji reaction. - content: ReactionContent! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # The reactable piece of content - reactable: Reactable! - - # Identifies the user who created this reaction. - user: User -} - -# A list of reactions that have been left on the subject. -type ReactionConnection { - # A list of edges. - edges: [ReactionEdge] - - # A list of nodes. - nodes: [Reaction] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! - - # Whether or not the authenticated user has left a reaction on the subject. - viewerHasReacted: Boolean! -} - -# Emojis that can be attached to Issues, Pull Requests and Comments. -enum ReactionContent { - # Represents the 😕 emoji. - CONFUSED - - # Represents the ❤️ emoji. - HEART - - # Represents the 🎉 emoji. - HOORAY - - # Represents the 😄 emoji. - LAUGH - - # Represents the 👎 emoji. - THUMBS_DOWN - - # Represents the 👍 emoji. - THUMBS_UP -} - -# An edge in a connection. -type ReactionEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Reaction -} - -# A group of emoji reactions to a particular piece of content. -type ReactionGroup { - # Identifies the emoji reaction. - content: ReactionContent! - - # Identifies when the reaction was created. - createdAt: DateTime - - # The subject that was reacted to. - subject: Reactable! - - # Users who have reacted to the reaction subject with the emotion represented by this reaction group - users( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ReactingUserConnection! - - # Whether or not the authenticated user has left a reaction on the subject. - viewerHasReacted: Boolean! -} - -# Ways in which lists of reactions can be ordered upon return. -input ReactionOrder { - # The direction in which to order reactions by the specified field. - direction: OrderDirection! - - # The field in which to order reactions by. - field: ReactionOrderField! -} - -# A list of fields that reactions can be ordered by. -enum ReactionOrderField { - # Allows ordering a list of reactions by when they were created. - CREATED_AT -} - -# Represents a Git reference. -type Ref implements Node { - # A list of pull requests with this ref as the head ref. - associatedPullRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # The base ref name to filter the pull requests by. - baseRefName: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # The head ref name to filter the pull requests by. - headRefName: String - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for pull requests returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the pull requests by. - states: [PullRequestState!] - ): PullRequestConnection! - id: ID! - - # The ref name. - name: String! - - # The ref's prefix, such as `refs/heads/` or `refs/tags/`. - prefix: String! - - # The repository the ref belongs to. - repository: Repository! - - # The object the ref points to. - target: GitObject! -} - -# The connection type for Ref. -type RefConnection { - # A list of edges. - edges: [RefEdge] - - # A list of nodes. - nodes: [Ref] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type RefEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Ref -} - -# Ways in which lists of git refs can be ordered upon return. -input RefOrder { - # The direction in which to order refs by the specified field. - direction: OrderDirection! - - # The field in which to order refs by. - field: RefOrderField! -} - -# Properties by which ref connections can be ordered. -enum RefOrderField { - # Order refs by their alphanumeric name - ALPHABETICAL - - # Order refs by underlying commit date if the ref prefix is refs/tags/ - TAG_COMMIT_DATE -} - -# Represents a 'referenced' event on a given `ReferencedSubject`. -type ReferencedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the commit associated with the 'referenced' event. - commit: Commit - - # Identifies the repository associated with the 'referenced' event. - commitRepository: Repository! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Reference originated in a different repository. - isCrossRepository: Boolean! - - # Checks if the commit message itself references the subject. Can be false in the case of a commit comment reference. - isDirectReference: Boolean! - - # Object referenced by event. - subject: ReferencedSubject! -} - -# Any referencable object -union ReferencedSubject = Issue | PullRequest - -# Represents an owner of a registry package. -interface RegistryPackageOwner { - id: ID! -} - -# Represents an interface to search packages on an object. -interface RegistryPackageSearch { - id: ID! -} - -# A release contains the content for a release. -type Release implements Node & UniformResourceLocatable { - # The author of the release - author: User - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the description of the release. - description: String - id: ID! - - # Whether or not the release is a draft - isDraft: Boolean! - - # Whether or not the release is a prerelease - isPrerelease: Boolean! - - # Identifies the title of the release. - name: String - - # Identifies the date and time when the release was created. - publishedAt: DateTime - - # List of releases assets which are dependent on this release. - releaseAssets( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # A list of names to filter the assets by. - name: String - ): ReleaseAssetConnection! - - # The HTTP path for this issue - resourcePath: URI! - - # The Git tag the release points to - tag: Ref - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this issue - url: URI! -} - -# A release asset contains the content for a release asset. -type ReleaseAsset implements Node { - # The asset's content-type - contentType: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The number of times this asset was downloaded - downloadCount: Int! - - # Identifies the URL where you can download the release asset via the browser. - downloadUrl: URI! - id: ID! - - # Identifies the title of the release asset. - name: String! - - # Release that the asset is associated with - release: Release - - # The size (in bytes) of the asset - size: Int! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The user that performed the upload - uploadedBy: User! - - # Identifies the URL of the release asset. - url: URI! -} - -# The connection type for ReleaseAsset. -type ReleaseAssetConnection { - # A list of edges. - edges: [ReleaseAssetEdge] - - # A list of nodes. - nodes: [ReleaseAsset] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ReleaseAssetEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ReleaseAsset -} - -# The connection type for Release. -type ReleaseConnection { - # A list of edges. - edges: [ReleaseEdge] - - # A list of nodes. - nodes: [Release] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ReleaseEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Release -} - -# Ways in which lists of releases can be ordered upon return. -input ReleaseOrder { - # The direction in which to order releases by the specified field. - direction: OrderDirection! - - # The field in which to order releases by. - field: ReleaseOrderField! -} - -# Properties by which release connections can be ordered. -enum ReleaseOrderField { - # Order releases by creation time - CREATED_AT - - # Order releases alphabetically by name - NAME -} - -# Autogenerated input type of RemoveOutsideCollaborator -input RemoveOutsideCollaboratorInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The ID of the organization to remove the outside collaborator from. - organizationId: ID! - - # The ID of the outside collaborator to remove. - userId: ID! -} - -# Autogenerated return type of RemoveOutsideCollaborator -type RemoveOutsideCollaboratorPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The user that was removed as an outside collaborator. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `removedUser` will change from `User!` to `User`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - removedUser: User! -} - -# Autogenerated input type of RemoveReaction -input RemoveReactionInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of the emoji reaction to remove. - content: ReactionContent! - - # The Node ID of the subject to modify. - subjectId: ID! -} - -# Autogenerated return type of RemoveReaction -type RemoveReactionPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The reaction object. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `reaction` will change from `Reaction!` to `Reaction`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - reaction: Reaction! - - # The reactable subject. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `subject` will change from `Reactable!` to `Reactable`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - subject: Reactable! -} - -# Autogenerated input type of RemoveStar -input RemoveStarInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Starrable ID to unstar. - starrableId: ID! -} - -# Autogenerated return type of RemoveStar -type RemoveStarPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The starrable. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `starrable` will change from `Starrable!` to `Starrable`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - starrable: Starrable! -} - -# Represents a 'removed_from_project' event on a given issue or pull request. -type RemovedFromProjectEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! -} - -# Represents a 'renamed' event on a given issue or pull request -type RenamedTitleEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the current title of the issue or pull request. - currentTitle: String! - id: ID! - - # Identifies the previous title of the issue or pull request. - previousTitle: String! - - # Subject that was renamed. - subject: RenamedTitleSubject! -} - -# An object which has a renamable title -union RenamedTitleSubject = Issue | PullRequest - -# Represents a 'reopened' event on any `Closable`. -type ReopenedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Object that was reopened. - closable: Closable! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! -} - -# A repository contains the content for a project. -type Repository implements Node & ProjectOwner & RegistryPackageOwner & RepositoryInfo & Starrable & Subscribable & UniformResourceLocatable { - # A list of users that can be assigned to issues in this repository. - assignableUsers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # Returns the code of conduct for this repository - codeOfConduct: CodeOfConduct - - # A list of collaborators associated with the repository. - collaborators( - # Collaborators affiliation level with a repository. - affiliation: CollaboratorAffiliation - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): RepositoryCollaboratorConnection - - # A list of commit comments associated with the repository. - commitComments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): CommitCommentConnection! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - - # The Ref associated with the repository's default branch. - defaultBranchRef: Ref - - # A list of deploy keys that are on this repository. - deployKeys( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): DeployKeyConnection! - - # Deployments associated with the repository - deployments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Environments to list deployments for - environments: [String!] - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): DeploymentConnection! - - # The description of the repository. - description: String - - # The description of the repository rendered to HTML. - descriptionHTML: HTML! - - # The number of kilobytes this repository occupies on disk. - diskUsage: Int - - # Returns how many forks there are of this repository in the whole network. - forkCount: Int! - - # A list of direct forked repositories. - forks( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # Indicates if the repository has issues feature enabled. - hasIssuesEnabled: Boolean! - - # Indicates if the repository has wiki feature enabled. - hasWikiEnabled: Boolean! - - # The repository's URL. - homepageUrl: URI - id: ID! - - # Indicates if the repository is unmaintained. - isArchived: Boolean! - - # Identifies if the repository is a fork. - isFork: Boolean! - - # Indicates if the repository has been locked or not. - isLocked: Boolean! - - # Identifies if the repository is a mirror. - isMirror: Boolean! - - # Identifies if the repository is private. - isPrivate: Boolean! - - # Returns a single issue from the current repository by number. - issue( - # The number for the issue to be returned. - number: Int! - ): Issue - - # Returns a single issue-like object from the current repository by number. - issueOrPullRequest( - # The number for the issue to be returned. - number: Int! - ): IssueOrPullRequest - - # A list of issues that have been opened in the repository. - issues( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for issues returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the issues by. - states: [IssueState!] - ): IssueConnection! - - # Returns a single label by name - label( - # Label name - name: String! - ): Label - - # A list of labels associated with the repository. - labels( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # If provided, searches labels by name and description. - query: String - ): LabelConnection - - # A list containing a breakdown of the language composition of the repository. - languages( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: LanguageOrder - ): LanguageConnection - - # The license associated with the repository - licenseInfo: License - - # The reason the repository has been locked. - lockReason: RepositoryLockReason - - # A list of Users that can be mentioned in the context of the repository. - mentionableUsers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! - - # Whether or not PRs are merged with a merge commit on this repository. - mergeCommitAllowed: Boolean! - - # Returns a single milestone from the current repository by number. - milestone( - # The number for the milestone to be returned. - number: Int! - ): Milestone - - # A list of milestones associated with the repository. - milestones( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for milestones. - orderBy: MilestoneOrder - - # Filter by the state of the milestones. - states: [MilestoneState!] - ): MilestoneConnection - - # The repository's original mirror URL. - mirrorUrl: URI - - # The name of the repository. - name: String! - - # The repository's name with owner. - nameWithOwner: String! - - # A Git object in the repository - object( - # A Git revision expression suitable for rev-parse - expression: String - - # The Git object ID - oid: GitObjectID - ): GitObject - - # The User owner of the repository. - owner: RepositoryOwner! - - # The repository parent, if this is a fork. - parent: Repository - - # The primary language of the repository's code. - primaryLanguage: Language - - # Find project by number. - project( - # The project number to find. - number: Int! - ): Project - - # A list of projects under the owner. - projects( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for projects returned from the connection - orderBy: ProjectOrder - - # Query to search projects by, currently only searching by name. - search: String - - # A list of states to filter the projects by. - states: [ProjectState!] - ): ProjectConnection! - - # The HTTP path listing the repository's projects - projectsResourcePath: URI! - - # The HTTP URL listing the repository's projects - projectsUrl: URI! - - # A list of protected branches that are on this repository. - protectedBranches( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): ProtectedBranchConnection! - - # Returns a single pull request from the current repository by number. - pullRequest( - # The number for the pull request to be returned. - number: Int! - ): PullRequest - - # A list of pull requests that have been opened in the repository. - pullRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # The base ref name to filter the pull requests by. - baseRefName: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # The head ref name to filter the pull requests by. - headRefName: String - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for pull requests returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the pull requests by. - states: [PullRequestState!] - ): PullRequestConnection! - - # Identifies when the repository was last pushed to. - pushedAt: DateTime - - # Whether or not rebase-merging is enabled on this repository. - rebaseMergeAllowed: Boolean! - - # Fetch a given ref from the repository - ref( - # The ref to retrieve. Fully qualified matches are checked in order - # (`refs/heads/master`) before falling back onto checks for short name matches (`master`). - qualifiedName: String! - ): Ref - - # Fetch a list of refs from the repository - refs( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # DEPRECATED: use orderBy. The ordering direction. - direction: OrderDirection - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for refs returned from the connection. - orderBy: RefOrder - - # A ref name prefix like `refs/heads/`, `refs/tags/`, etc. - refPrefix: String! - ): RefConnection - - # Lookup a single release given various criteria. - release( - # The name of the Tag the Release was created from - tagName: String! - ): Release - - # List of releases which are dependent on this repository. - releases( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: ReleaseOrder - ): ReleaseConnection! - - # A list of applied repository-topic associations for this repository. - repositoryTopics( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): RepositoryTopicConnection! - - # The HTTP path for this repository - resourcePath: URI! - - # A description of the repository, rendered to HTML without any links in it. - shortDescriptionHTML( - # How many characters to return. - limit: Int = 200 - ): HTML! - - # Whether or not squash-merging is enabled on this repository. - squashMergeAllowed: Boolean! - - # The SSH URL to clone this repository - sshUrl: GitSSHRemote! - - # A list of users who have starred this starrable. - stargazers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: StarOrder - ): StargazerConnection! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this repository - url: URI! - - # Indicates whether the viewer has admin permissions on this repository. - viewerCanAdminister: Boolean! - - # Can the current viewer create new projects on this owner. - viewerCanCreateProjects: Boolean! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Indicates whether the viewer can update the topics of this repository. - viewerCanUpdateTopics: Boolean! - - # Returns a boolean indicating whether the viewing user has starred this starrable. - viewerHasStarred: Boolean! - - # The users permission level on the repository. Will return null if authenticated as an GitHub App. - viewerPermission: RepositoryPermission - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState - - # A list of users watching the repository. - watchers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): UserConnection! -} - -# The affiliation of a user to a repository -enum RepositoryAffiliation { - # Repositories that the user has been added to as a collaborator. - COLLABORATOR - - # Repositories that the user has access to through being a member of an - # organization. This includes every repository on every team that the user is on. - ORGANIZATION_MEMBER - - # Repositories that are owned by the authenticated user. - OWNER -} - -# The affiliation type between collaborator and repository. -enum RepositoryCollaboratorAffiliation { - # All collaborators of the repository. - ALL - - # All outside collaborators of an organization-owned repository. - OUTSIDE -} - -# The connection type for User. -type RepositoryCollaboratorConnection { - # A list of edges. - edges: [RepositoryCollaboratorEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a user who is a collaborator of a repository. -type RepositoryCollaboratorEdge { - # A cursor for use in pagination. - cursor: String! - node: User! - - # The permission the user has on the repository. - permission: RepositoryPermission! -} - -# A list of repositories owned by the subject. -type RepositoryConnection { - # A list of edges. - edges: [RepositoryEdge] - - # A list of nodes. - nodes: [Repository] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! - - # The total size in kilobytes of all repositories in the connection. - totalDiskUsage: Int! -} - -# The reason a repository is listed as 'contributed'. -enum RepositoryContributionType { - # Created a commit - COMMIT - - # Created an issue - ISSUE - - # Created a pull request - PULL_REQUEST - - # Reviewed a pull request - PULL_REQUEST_REVIEW - - # Created the repository - REPOSITORY -} - -# An edge in a connection. -type RepositoryEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Repository -} - -# A subset of repository info. -interface RepositoryInfo { - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The description of the repository. - description: String - - # The description of the repository rendered to HTML. - descriptionHTML: HTML! - - # Returns how many forks there are of this repository in the whole network. - forkCount: Int! - - # Indicates if the repository has issues feature enabled. - hasIssuesEnabled: Boolean! - - # Indicates if the repository has wiki feature enabled. - hasWikiEnabled: Boolean! - - # The repository's URL. - homepageUrl: URI - - # Indicates if the repository is unmaintained. - isArchived: Boolean! - - # Identifies if the repository is a fork. - isFork: Boolean! - - # Indicates if the repository has been locked or not. - isLocked: Boolean! - - # Identifies if the repository is a mirror. - isMirror: Boolean! - - # Identifies if the repository is private. - isPrivate: Boolean! - - # The license associated with the repository - licenseInfo: License - - # The reason the repository has been locked. - lockReason: RepositoryLockReason - - # The repository's original mirror URL. - mirrorUrl: URI - - # The name of the repository. - name: String! - - # The repository's name with owner. - nameWithOwner: String! - - # The User owner of the repository. - owner: RepositoryOwner! - - # Identifies when the repository was last pushed to. - pushedAt: DateTime - - # The HTTP path for this repository - resourcePath: URI! - - # A description of the repository, rendered to HTML without any links in it. - shortDescriptionHTML( - # How many characters to return. - limit: Int = 200 - ): HTML! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this repository - url: URI! -} - -# An invitation for a user to be added to a repository. -type RepositoryInvitation implements Node { - id: ID! - - # The user who received the invitation. - invitee: User! - - # The user who created the invitation. - inviter: User! - - # The permission granted on this repository by this invitation. - permission: RepositoryPermission! - - # The Repository the user is invited to. - repository: RepositoryInfo -} - -# An edge in a connection. -type RepositoryInvitationEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: RepositoryInvitation -} - -# The possible reasons a given repository could be in a locked state. -enum RepositoryLockReason { - # The repository is locked due to a billing related reason. - BILLING - - # The repository is locked due to a migration. - MIGRATING - - # The repository is locked due to a move. - MOVING - - # The repository is locked due to a rename. - RENAME -} - -# Represents a object that belongs to a repository. -interface RepositoryNode { - # The repository associated with this node. - repository: Repository! -} - -# Ordering options for repository connections -input RepositoryOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order repositories by. - field: RepositoryOrderField! -} - -# Properties by which repository connections can be ordered. -enum RepositoryOrderField { - # Order repositories by creation time - CREATED_AT - - # Order repositories by name - NAME - - # Order repositories by push time - PUSHED_AT - - # Order repositories by number of stargazers - STARGAZERS - - # Order repositories by update time - UPDATED_AT -} - -# Represents an owner of a Repository. -interface RepositoryOwner { - # A URL pointing to the owner's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - id: ID! - - # The username used to login. - login: String! - - # A list of repositories this user has pinned to their profile - pinnedRepositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # A list of repositories that the user owns. - repositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they are forks of another repository - isFork: Boolean - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # Find Repository. - repository( - # Name of Repository to find. - name: String! - ): Repository - - # The HTTP URL for the owner. - resourcePath: URI! - - # The HTTP URL for the owner. - url: URI! -} - -# The access level to a repository -enum RepositoryPermission { - # Can read, clone, push, and add collaborators - ADMIN - - # Can read and clone - READ - - # Can read, clone and push - WRITE -} - -# The privacy of a repository -enum RepositoryPrivacy { - # Private - PRIVATE - - # Public - PUBLIC -} - -# A repository-topic connects a repository to a topic. -type RepositoryTopic implements Node & UniformResourceLocatable { - id: ID! - - # The HTTP path for this repository-topic. - resourcePath: URI! - - # The topic. - topic: Topic! - - # The HTTP URL for this repository-topic. - url: URI! -} - -# The connection type for RepositoryTopic. -type RepositoryTopicConnection { - # A list of edges. - edges: [RepositoryTopicEdge] - - # A list of nodes. - nodes: [RepositoryTopic] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type RepositoryTopicEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: RepositoryTopic -} - -# Autogenerated input type of RequestReviews -input RequestReviewsInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the pull request to modify. - pullRequestId: ID! - - # The Node IDs of the team to request. - teamIds: [ID!] - - # Add users to the set rather than replace. - union: Boolean - - # The Node IDs of the user to request. - userIds: [ID!] -} - -# Autogenerated return type of RequestReviews -type RequestReviewsPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The pull request that is getting requests. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequest` will change from `PullRequest!` to `PullRequest`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequest: PullRequest! - - # The edge from the pull request to the requested reviewers. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `requestedReviewersEdge` will change from `UserEdge!` to `UserEdge`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - requestedReviewersEdge: UserEdge! -} - -# Types that can be requested reviewers. -union RequestedReviewer = Team | User - -# A team or user who has the ability to dismiss a review on a protected branch. -type ReviewDismissalAllowance implements Node { - # The actor that can dismiss. - actor: ReviewDismissalAllowanceActor - id: ID! - - # Identifies the protected branch associated with the allowed user or team. - protectedBranch: ProtectedBranch! -} - -# Types that can be an actor. -union ReviewDismissalAllowanceActor = Team | User - -# The connection type for ReviewDismissalAllowance. -type ReviewDismissalAllowanceConnection { - # A list of edges. - edges: [ReviewDismissalAllowanceEdge] - - # A list of nodes. - nodes: [ReviewDismissalAllowance] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ReviewDismissalAllowanceEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ReviewDismissalAllowance -} - -# Represents a 'review_dismissed' event on a given issue or pull request. -type ReviewDismissedEvent implements Node & UniformResourceLocatable { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # Identifies the message associated with the 'review_dismissed' event. - message: String! - - # The message associated with the event, rendered to HTML. - messageHtml: HTML! - - # Identifies the previous state of the review with the 'review_dismissed' event. - previousReviewState: PullRequestReviewState! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # Identifies the commit which caused the review to become stale. - pullRequestCommit: PullRequestCommit - - # The HTTP path for this review dismissed event. - resourcePath: URI! - - # Identifies the review associated with the 'review_dismissed' event. - review: PullRequestReview - - # The HTTP URL for this review dismissed event. - url: URI! -} - -# A request for a user to review a pull request. -type ReviewRequest implements Node { - # Identifies the primary key from the database. - databaseId: Int - id: ID! - - # Identifies the pull request associated with this review request. - pullRequest: PullRequest! - - # The reviewer that is requested. - requestedReviewer: RequestedReviewer -} - -# The connection type for ReviewRequest. -type ReviewRequestConnection { - # A list of edges. - edges: [ReviewRequestEdge] - - # A list of nodes. - nodes: [ReviewRequest] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type ReviewRequestEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: ReviewRequest -} - -# Represents an 'review_request_removed' event on a given pull request. -type ReviewRequestRemovedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # Identifies the reviewer whose review request was removed. - requestedReviewer: RequestedReviewer -} - -# Represents an 'review_requested' event on a given pull request. -type ReviewRequestedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # PullRequest referenced by event. - pullRequest: PullRequest! - - # Identifies the reviewer whose review was requested. - requestedReviewer: RequestedReviewer -} - -# The results of a search. -union SearchResultItem = Issue | MarketplaceListing | Organization | PullRequest | Repository | User - -# A list of results that matched against a search query. -type SearchResultItemConnection { - # The number of pieces of code that matched the search query. - codeCount: Int! - - # A list of edges. - edges: [SearchResultItemEdge] - - # The number of issues that matched the search query. - issueCount: Int! - - # A list of nodes. - nodes: [SearchResultItem] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # The number of repositories that matched the search query. - repositoryCount: Int! - - # The number of users that matched the search query. - userCount: Int! - - # The number of wiki pages that matched the search query. - wikiCount: Int! -} - -# An edge in a connection. -type SearchResultItemEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: SearchResultItem - - # Text matches on the result found. - textMatches: [TextMatch] -} - -# Represents the individual results of a search. -enum SearchType { - # Returns results matching issues in repositories. - ISSUE - - # Returns results matching repositories. - REPOSITORY - - # Returns results matching users and organizations on GitHub. - USER -} - -# Represents an S/MIME signature on a Commit or Tag. -type SmimeSignature implements GitSignature { - # Email used to sign this object. - email: String! - - # True if the signature is valid and verified by GitHub. - isValid: Boolean! - - # Payload for GPG signing object. Raw ODB object without the signature header. - payload: String! - - # ASCII-armored signature header from object. - signature: String! - - # GitHub user corresponding to the email signing this commit. - signer: User - - # The state of this signature. `VALID` if signature is valid and verified by - # GitHub, otherwise represents reason why signature is considered invalid. - state: GitSignatureState! - - # True if the signature was made with GitHub's signing key. - wasSignedByGitHub: Boolean! -} - -# Ways in which star connections can be ordered. -input StarOrder { - # The direction in which to order nodes. - direction: OrderDirection! - - # The field in which to order nodes by. - field: StarOrderField! -} - -# Properties by which star connections can be ordered. -enum StarOrderField { - # Allows ordering a list of stars by when they were created. - STARRED_AT -} - -# The connection type for User. -type StargazerConnection { - # A list of edges. - edges: [StargazerEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a user that's starred a repository. -type StargazerEdge { - # A cursor for use in pagination. - cursor: String! - node: User! - - # Identifies when the item was starred. - starredAt: DateTime! -} - -# Things that can be starred. -interface Starrable { - id: ID! - - # A list of users who have starred this starrable. - stargazers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: StarOrder - ): StargazerConnection! - - # Returns a boolean indicating whether the viewing user has starred this starrable. - viewerHasStarred: Boolean! -} - -# The connection type for Repository. -type StarredRepositoryConnection { - # A list of edges. - edges: [StarredRepositoryEdge] - - # A list of nodes. - nodes: [Repository] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a starred repository. -type StarredRepositoryEdge { - # A cursor for use in pagination. - cursor: String! - node: Repository! - - # Identifies when the item was starred. - starredAt: DateTime! -} - -# Represents a commit status. -type Status implements Node { - # The commit this status is attached to. - commit: Commit - - # Looks up an individual status context by context name. - context( - # The context name. - name: String! - ): StatusContext - - # The individual status contexts for this commit. - contexts: [StatusContext!]! - id: ID! - - # The combined commit status. - state: StatusState! -} - -# Represents an individual commit status context -type StatusContext implements Node { - # This commit this status context is attached to. - commit: Commit - - # The name of this status context. - context: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The actor who created this status context. - creator: Actor - - # The description for this status context. - description: String - id: ID! - - # The state of this status context. - state: StatusState! - - # The URL for this status context. - targetUrl: URI -} - -# The possible commit status states. -enum StatusState { - # Status is errored. - ERROR - - # Status is expected. - EXPECTED - - # Status is failing. - FAILURE - - # Status is pending. - PENDING - - # Status is successful. - SUCCESS -} - -# Autogenerated input type of SubmitPullRequestReview -input SubmitPullRequestReviewInput { - # The text field to set on the Pull Request Review. - body: String - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The event to send to the Pull Request Review. - event: PullRequestReviewEvent! - - # The Pull Request Review ID to submit. - pullRequestReviewId: ID! -} - -# Autogenerated return type of SubmitPullRequestReview -type SubmitPullRequestReviewPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The submitted pull request review. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReview: PullRequestReview! -} - -# Entities that can be subscribed to for web and email notifications. -interface Subscribable { - id: ID! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState -} - -# Represents a 'subscribed' event on a given `Subscribable`. -type SubscribedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Object referenced by event. - subscribable: Subscribable! -} - -# The possible states of a subscription. -enum SubscriptionState { - # The User is never notified. - IGNORED - - # The User is notified of all conversations. - SUBSCRIBED - - # The User is only notified when particpating or @mentioned. - UNSUBSCRIBED -} - -# A suggestion to review a pull request based on a user's commit history and review comments. -type SuggestedReviewer { - # Is this suggestion based on past commits? - isAuthor: Boolean! - - # Is this suggestion based on past review comments? - isCommenter: Boolean! - - # Identifies the user suggested to review the pull request. - reviewer: User! -} - -# Represents a Git tag. -type Tag implements GitObject & Node { - # An abbreviated version of the Git object ID - abbreviatedOid: String! - - # The HTTP path for this Git object - commitResourcePath: URI! - - # The HTTP URL for this Git object - commitUrl: URI! - id: ID! - - # The Git tag message. - message: String - - # The Git tag name. - name: String! - - # The Git object ID - oid: GitObjectID! - - # The Repository the Git object belongs to - repository: Repository! - - # Details about the tag author. - tagger: GitActor - - # The Git object the tag points to. - target: GitObject! -} - -# A team of users in an organization. -type Team implements Node & Subscribable { - # A list of teams that are ancestors of this team. - ancestors( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): TeamConnection! - - # A URL pointing to the team's avatar. - avatarUrl( - # The size in pixels of the resulting square image. - size: Int = 400 - ): URI - - # List of child teams belonging to this team - childTeams( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Whether to list immediate child teams or all descendant child teams. - immediateOnly: Boolean = true - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: TeamOrder - - # User logins to filter by - userLogins: [String!] - ): TeamConnection! - - # The slug corresponding to the organization and team. - combinedSlug: String! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # The description of the team. - description: String - - # The HTTP path for editing this team - editTeamResourcePath: URI! - - # The HTTP URL for editing this team - editTeamUrl: URI! - id: ID! - - # A list of pending invitations for users to this team - invitations( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): OrganizationInvitationConnection - - # A list of users who are members of this team. - members( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Filter by membership type - membership: TeamMembershipType = ALL - - # Order for the connection. - orderBy: TeamMemberOrder - - # The search string to look for. - query: String - - # Filter by team member role - role: TeamMemberRole - ): TeamMemberConnection! - - # The HTTP path for the team' members - membersResourcePath: URI! - - # The HTTP URL for the team' members - membersUrl: URI! - - # The name of the team. - name: String! - - # The HTTP path creating a new team - newTeamResourcePath: URI! - - # The HTTP URL creating a new team - newTeamUrl: URI! - - # The organization that owns this team. - organization: Organization! - - # The parent team of the team. - parentTeam: Team - - # The level of privacy the team has. - privacy: TeamPrivacy! - - # A list of repositories this team has access to. - repositories( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for the connection. - orderBy: TeamRepositoryOrder - - # The search string to look for. - query: String - ): TeamRepositoryConnection! - - # The HTTP path for this team's repositories - repositoriesResourcePath: URI! - - # The HTTP URL for this team's repositories - repositoriesUrl: URI! - - # The HTTP path for this team - resourcePath: URI! - - # The slug corresponding to the team. - slug: String! - - # The HTTP path for this team's teams - teamsResourcePath: URI! - - # The HTTP URL for this team's teams - teamsUrl: URI! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this team - url: URI! - - # Team is adminable by the viewer. - viewerCanAdminister: Boolean! - - # Check if the viewer is able to change their subscription status for the repository. - viewerCanSubscribe: Boolean! - - # Identifies if the viewer is watching, not watching, or ignoring the subscribable entity. - viewerSubscription: SubscriptionState -} - -# The connection type for Team. -type TeamConnection { - # A list of edges. - edges: [TeamEdge] - - # A list of nodes. - nodes: [Team] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type TeamEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Team -} - -# The connection type for User. -type TeamMemberConnection { - # A list of edges. - edges: [TeamMemberEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a user who is a member of a team. -type TeamMemberEdge { - # A cursor for use in pagination. - cursor: String! - - # The HTTP path to the organization's member access page. - memberAccessResourcePath: URI! - - # The HTTP URL to the organization's member access page. - memberAccessUrl: URI! - node: User! - - # The role the member has on the team. - role: TeamMemberRole! -} - -# Ordering options for team member connections -input TeamMemberOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order team members by. - field: TeamMemberOrderField! -} - -# Properties by which team member connections can be ordered. -enum TeamMemberOrderField { - # Order team members by creation time - CREATED_AT - - # Order team members by login - LOGIN -} - -# The possible team member roles; either 'maintainer' or 'member'. -enum TeamMemberRole { - # A team maintainer has permission to add and remove team members. - MAINTAINER - - # A team member has no administrative permissions on the team. - MEMBER -} - -# Defines which types of team members are included in the returned list. Can be one of IMMEDIATE, CHILD_TEAM or ALL. -enum TeamMembershipType { - # Includes immediate and child team members for the team. - ALL - - # Includes only child team members for the team. - CHILD_TEAM - - # Includes only immediate members of the team. - IMMEDIATE -} - -# Ways in which team connections can be ordered. -input TeamOrder { - # The direction in which to order nodes. - direction: OrderDirection! - - # The field in which to order nodes by. - field: TeamOrderField! -} - -# Properties by which team connections can be ordered. -enum TeamOrderField { - # Allows ordering a list of teams by name. - NAME -} - -# The possible team privacy values. -enum TeamPrivacy { - # A secret team can only be seen by its members. - SECRET - - # A visible team can be seen and @mentioned by every member of the organization. - VISIBLE -} - -# The connection type for Repository. -type TeamRepositoryConnection { - # A list of edges. - edges: [TeamRepositoryEdge] - - # A list of nodes. - nodes: [Repository] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# Represents a team repository. -type TeamRepositoryEdge { - # A cursor for use in pagination. - cursor: String! - node: Repository! - - # The permission level the team has on the repository - permission: RepositoryPermission! -} - -# Ordering options for team repository connections -input TeamRepositoryOrder { - # The ordering direction. - direction: OrderDirection! - - # The field to order repositories by. - field: TeamRepositoryOrderField! -} - -# Properties by which team repository connections can be ordered. -enum TeamRepositoryOrderField { - # Order repositories by creation time - CREATED_AT - - # Order repositories by name - NAME - - # Order repositories by permission - PERMISSION - - # Order repositories by push time - PUSHED_AT - - # Order repositories by number of stargazers - STARGAZERS - - # Order repositories by update time - UPDATED_AT -} - -# The role of a user on a team. -enum TeamRole { - # User has admin rights on the team. - ADMIN - - # User is a member of the team. - MEMBER -} - -# A text match within a search result. -type TextMatch { - # The specific text fragment within the property matched on. - fragment: String! - - # Highlights within the matched fragment. - highlights: [TextMatchHighlight!]! - - # The property matched on. - property: String! -} - -# Represents a single highlight in a search result match. -type TextMatchHighlight { - # The indice in the fragment where the matched text begins. - beginIndice: Int! - - # The indice in the fragment where the matched text ends. - endIndice: Int! - - # The text matched. - text: String! -} - -# A topic aggregates entities that are related to a subject. -type Topic implements Node { - id: ID! - - # The topic's name. - name: String! - - # A list of related topics, including aliases of this topic, sorted with the most relevant - # first. - relatedTopics: [Topic!]! -} - -# The connection type for Topic. -type TopicConnection { - # A list of edges. - edges: [TopicEdge] - - # A list of nodes. - nodes: [Topic] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type TopicEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: Topic -} - -# Reason that the suggested topic is declined. -enum TopicSuggestionDeclineReason { - # The suggested topic is not relevant to the repository. - NOT_RELEVANT - - # The viewer does not like the suggested topic. - PERSONAL_PREFERENCE - - # The suggested topic is too general for the repository. - TOO_GENERAL - - # The suggested topic is too specific for the repository (e.g. #ruby-on-rails-version-4-2-1). - TOO_SPECIFIC -} - -# Represents a Git tree. -type Tree implements GitObject & Node { - # An abbreviated version of the Git object ID - abbreviatedOid: String! - - # The HTTP path for this Git object - commitResourcePath: URI! - - # The HTTP URL for this Git object - commitUrl: URI! - - # A list of tree entries. - entries: [TreeEntry!] - id: ID! - - # The Git object ID - oid: GitObjectID! - - # The Repository the Git object belongs to - repository: Repository! -} - -# Represents a Git tree entry. -type TreeEntry { - # Entry file mode. - mode: Int! - - # Entry file name. - name: String! - - # Entry file object. - object: GitObject - - # Entry file Git object ID. - oid: GitObjectID! - - # The Repository the tree entry belongs to - repository: Repository! - - # Entry file type. - type: String! -} - -# An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. -scalar URI - -# Represents an 'unassigned' event on any assignable object. -type UnassignedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the assignable associated with the event. - assignable: Assignable! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the subject (user) who was unassigned. - user: User -} - -# Represents a type that can be retrieved by a URL. -interface UniformResourceLocatable { - # The HTML path to this resource. - resourcePath: URI! - - # The URL to this resource. - url: URI! -} - -# Represents an unknown signature on a Commit or Tag. -type UnknownSignature implements GitSignature { - # Email used to sign this object. - email: String! - - # True if the signature is valid and verified by GitHub. - isValid: Boolean! - - # Payload for GPG signing object. Raw ODB object without the signature header. - payload: String! - - # ASCII-armored signature header from object. - signature: String! - - # GitHub user corresponding to the email signing this commit. - signer: User - - # The state of this signature. `VALID` if signature is valid and verified by - # GitHub, otherwise represents reason why signature is considered invalid. - state: GitSignatureState! - - # True if the signature was made with GitHub's signing key. - wasSignedByGitHub: Boolean! -} - -# Represents an 'unlabeled' event on a given issue or pull request. -type UnlabeledEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Identifies the label associated with the 'unlabeled' event. - label: Label! - - # Identifies the `Labelable` associated with the event. - labelable: Labelable! -} - -# Autogenerated input type of UnlockLockable -input UnlockLockableInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # ID of the issue or pull request to be unlocked. - lockableId: ID! -} - -# Autogenerated return type of UnlockLockable -type UnlockLockablePayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The item that was unlocked. - unlockedRecord: Lockable -} - -# Represents an 'unlocked' event on a given issue or pull request. -type UnlockedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Object that was unlocked. - lockable: Lockable! -} - -# Represents an 'unsubscribed' event on a given `Subscribable`. -type UnsubscribedEvent implements Node { - # Identifies the actor who performed the event. - actor: Actor - - # Identifies the date and time when the object was created. - createdAt: DateTime! - id: ID! - - # Object referenced by event. - subscribable: Subscribable! -} - -# Entities that can be updated. -interface Updatable { - # Check if the current viewer can update this object. - viewerCanUpdate: Boolean! -} - -# Comments that can be updated. -interface UpdatableComment { - # Reasons why the current viewer can not update this comment. - viewerCannotUpdateReasons: [CommentCannotUpdateReason!]! -} - -# Autogenerated input type of UpdateProjectCard -input UpdateProjectCardInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # Whether or not the ProjectCard should be archived - isArchived: Boolean - - # The note of ProjectCard. - note: String - - # The ProjectCard ID to update. - projectCardId: ID! -} - -# Autogenerated return type of UpdateProjectCard -type UpdateProjectCardPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The updated ProjectCard. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `projectCard` will change from `ProjectCard!` to `ProjectCard`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - projectCard: ProjectCard! -} - -# Autogenerated input type of UpdateProjectColumn -input UpdateProjectColumnInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of project column. - name: String! - - # The ProjectColumn ID to update. - projectColumnId: ID! -} - -# Autogenerated return type of UpdateProjectColumn -type UpdateProjectColumnPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The updated project column. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `projectColumn` will change from `ProjectColumn!` to `ProjectColumn`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - projectColumn: ProjectColumn! -} - -# Autogenerated input type of UpdateProject -input UpdateProjectInput { - # The description of project. - body: String - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The name of project. - name: String - - # The Project ID to update. - projectId: ID! - - # Whether the project is public or not. - public: Boolean - - # Whether the project is open or closed. - state: ProjectState -} - -# Autogenerated return type of UpdateProject -type UpdateProjectPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The updated project. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `project` will change from `Project!` to `Project`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - project: Project! -} - -# Autogenerated input type of UpdatePullRequestReviewComment -input UpdatePullRequestReviewCommentInput { - # The text of the comment. - body: String! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the comment to modify. - pullRequestReviewCommentId: ID! -} - -# Autogenerated return type of UpdatePullRequestReviewComment -type UpdatePullRequestReviewCommentPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The updated comment. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReviewComment` will change from - # `PullRequestReviewComment!` to `PullRequestReviewComment`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReviewComment: PullRequestReviewComment! -} - -# Autogenerated input type of UpdatePullRequestReview -input UpdatePullRequestReviewInput { - # The contents of the pull request review body. - body: String! - - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the pull request review to modify. - pullRequestReviewId: ID! -} - -# Autogenerated return type of UpdatePullRequestReview -type UpdatePullRequestReviewPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The updated pull request review. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `pullRequestReview` will change from `PullRequestReview!` to `PullRequestReview`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - pullRequestReview: PullRequestReview! -} - -# Autogenerated input type of UpdateSubscription -input UpdateSubscriptionInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The new state of the subscription. - state: SubscriptionState! - - # The Node ID of the subscribable object to modify. - subscribableId: ID! -} - -# Autogenerated return type of UpdateSubscription -type UpdateSubscriptionPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The input subscribable entity. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `subscribable` will change from `Subscribable!` to `Subscribable`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - subscribable: Subscribable! -} - -# Autogenerated input type of UpdateTopics -input UpdateTopicsInput { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # The Node ID of the repository. - repositoryId: ID! - - # An array of topic names. - topicNames: [String!]! -} - -# Autogenerated return type of UpdateTopics -type UpdateTopicsPayload { - # A unique identifier for the client performing the mutation. - clientMutationId: String - - # Names of the provided topics that are not valid. - invalidTopicNames: [String!] - - # The updated repository. - # - # **Upcoming Change on 2019-01-01 UTC** - # **Description:** Type for `repository` will change from `Repository!` to `Repository`. - # **Reason:** In preparation for an upcoming change to the way we report - # mutation errors, non-nullable payload fields are becoming nullable. - repository: Repository! -} - -# A user is an individual's account on GitHub that owns repositories and can make new content. -type User implements Actor & Node & RegistryPackageOwner & RegistryPackageSearch & RepositoryOwner & UniformResourceLocatable { - # A URL pointing to the user's public avatar. - avatarUrl( - # The size of the resulting square image. - size: Int - ): URI! - - # The user's public profile bio. - bio: String - - # The user's public profile bio as HTML. - bioHTML: HTML! - - # A list of commit comments made by this user. - commitComments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): CommitCommentConnection! - - # The user's public profile company. - company: String - - # The user's public profile company as HTML. - companyHTML: HTML! - - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the primary key from the database. - databaseId: Int - - # The user's publicly visible profile email. - email: String! - - # A list of users the given user is followed by. - followers( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): FollowerConnection! - - # A list of users the given user is following. - following( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): FollowingConnection! - - # Find gist by repo name. - gist( - # The gist name to find. - name: String! - ): Gist - - # A list of gist comments made by this user. - gistComments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): GistCommentConnection! - - # A list of the Gists the user has created. - gists( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for gists returned from the connection - orderBy: GistOrder - - # Filters Gists according to privacy. - privacy: GistPrivacy - ): GistConnection! - id: ID! - - # Whether or not this user is a participant in the GitHub Security Bug Bounty. - isBountyHunter: Boolean! - - # Whether or not this user is a participant in the GitHub Campus Experts Program. - isCampusExpert: Boolean! - - # Whether or not this user is a GitHub Developer Program member. - isDeveloperProgramMember: Boolean! - - # Whether or not this user is a GitHub employee. - isEmployee: Boolean! - - # Whether or not the user has marked themselves as for hire. - isHireable: Boolean! - - # Whether or not this user is a site administrator. - isSiteAdmin: Boolean! - - # Whether or not this user is the viewing user. - isViewer: Boolean! - - # A list of issue comments made by this user. - issueComments( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): IssueCommentConnection! - - # A list of issues associated with this user. - issues( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for issues returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the issues by. - states: [IssueState!] - ): IssueConnection! - - # The user's public profile location. - location: String - - # The username used to login. - login: String! - - # The user's public profile name. - name: String - - # Find an organization by its login that the user belongs to. - organization( - # The login of the organization to find. - login: String! - ): Organization - - # A list of organizations the user belongs to. - organizations( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): OrganizationConnection! - - # A list of repositories this user has pinned to their profile - pinnedRepositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # A list of public keys associated with this user. - publicKeys( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - ): PublicKeyConnection! - - # A list of pull requests associated with this user. - pullRequests( - # Returns the elements in the list that come after the specified cursor. - after: String - - # The base ref name to filter the pull requests by. - baseRefName: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # The head ref name to filter the pull requests by. - headRefName: String - - # A list of label names to filter the pull requests by. - labels: [String!] - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for pull requests returned from the connection. - orderBy: IssueOrder - - # A list of states to filter the pull requests by. - states: [PullRequestState!] - ): PullRequestConnection! - - # A list of repositories that the user owns. - repositories( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they are forks of another repository - isFork: Boolean - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # A list of repositories that the user recently contributed to. - repositoriesContributedTo( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # If non-null, include only the specified types of contributions. The - # GitHub.com UI uses [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY] - contributionTypes: [RepositoryContributionType] - - # Returns the first _n_ elements from the list. - first: Int - - # If true, include user repositories - includeUserRepositories: Boolean - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # Find Repository. - repository( - # Name of Repository to find. - name: String! - ): Repository - - # The HTTP path for this user - resourcePath: URI! - - # Repositories the user has starred. - starredRepositories( - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # Returns the last _n_ elements from the list. - last: Int - - # Order for connection - orderBy: StarOrder - - # Filters starred repositories to only return repositories owned by the viewer. - ownedByViewer: Boolean - ): StarredRepositoryConnection! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! - - # The HTTP URL for this user - url: URI! - - # Whether or not the viewer is able to follow the user. - viewerCanFollow: Boolean! - - # Whether or not this user is followed by the viewer. - viewerIsFollowing: Boolean! - - # A list of repositories the given user is watching. - watching( - # Affiliation options for repositories returned from the connection - affiliations: [RepositoryAffiliation] = [OWNER, COLLABORATOR, ORGANIZATION_MEMBER] - - # Returns the elements in the list that come after the specified cursor. - after: String - - # Returns the elements in the list that come before the specified cursor. - before: String - - # Returns the first _n_ elements from the list. - first: Int - - # If non-null, filters repositories according to whether they have been locked - isLocked: Boolean - - # Returns the last _n_ elements from the list. - last: Int - - # Ordering options for repositories returned from the connection - orderBy: RepositoryOrder - - # If non-null, filters repositories according to privacy - privacy: RepositoryPrivacy - ): RepositoryConnection! - - # A URL pointing to the user's public website/blog. - websiteUrl: URI -} - -# The connection type for User. -type UserConnection { - # A list of edges. - edges: [UserEdge] - - # A list of nodes. - nodes: [User] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edit on user content -type UserContentEdit implements Node { - # Identifies the date and time when the object was created. - createdAt: DateTime! - - # Identifies the date and time when the object was deleted. - deletedAt: DateTime - - # The actor who deleted this content - deletedBy: Actor - - # A summary of the changes for this edit - diff: String - - # When this content was edited - editedAt: DateTime! - - # The actor who edited this content - editor: Actor - id: ID! - - # Identifies the date and time when the object was last updated. - updatedAt: DateTime! -} - -# A list of edits to content. -type UserContentEditConnection { - # A list of edges. - edges: [UserContentEditEdge] - - # A list of nodes. - nodes: [UserContentEdit] - - # Information to aid in pagination. - pageInfo: PageInfo! - - # Identifies the total count of items in the connection. - totalCount: Int! -} - -# An edge in a connection. -type UserContentEditEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: UserContentEdit -} - -# Represents a user. -type UserEdge { - # A cursor for use in pagination. - cursor: String! - - # The item at the end of the edge. - node: User -} - -# A valid x509 certificate string -scalar X509Certificate diff --git a/src/ghstack/ghstack/github_utils.py b/src/ghstack/ghstack/github_utils.py deleted file mode 100644 index ecb4b70..0000000 --- a/src/ghstack/ghstack/github_utils.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 - -import re -from typing import Optional - -from typing_extensions import TypedDict - -import ghstack.github -import ghstack.shell -from ghstack.types import GitHubRepositoryId - -GitHubRepoNameWithOwner = TypedDict( - "GitHubRepoNameWithOwner", - { - "owner": str, - "name": str, - }, -) - - -def get_github_repo_name_with_owner( - *, - sh: ghstack.shell.Shell, - github_url: str, - remote_name: str, -) -> GitHubRepoNameWithOwner: - # Grovel in remotes to figure it out - remote_url = sh.git("remote", "get-url", remote_name) - while True: - match = r"^git@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format( - github_url=github_url - ) - m = re.match(match, remote_url) - if m: - owner = m.group(1) - name = m.group(2) - break - search = r"{github_url}/([^/]+)/(.+?)(?:\.git)?$".format(github_url=github_url) - m = re.search(search, remote_url) - if m: - owner = m.group(1) - name = m.group(2) - break - raise RuntimeError( - "Couldn't determine repo owner and name from url: {}".format(remote_url) - ) - return {"owner": owner, "name": name} - - -GitHubRepoInfo = TypedDict( - "GitHubRepoInfo", - { - "name_with_owner": GitHubRepoNameWithOwner, - "id": GitHubRepositoryId, - "is_fork": bool, - "default_branch": str, - }, -) - - -def get_github_repo_info( - *, - github: ghstack.github.GitHubEndpoint, - sh: ghstack.shell.Shell, - repo_owner: Optional[str] = None, - repo_name: Optional[str] = None, - github_url: str, - remote_name: str, -) -> GitHubRepoInfo: - if repo_owner is None or repo_name is None: - name_with_owner = get_github_repo_name_with_owner( - sh=sh, - github_url=github_url, - remote_name=remote_name, - ) - else: - name_with_owner = {"owner": repo_owner, "name": repo_name} - - # TODO: Cache this guy - repo = github.graphql( - """ - query ($owner: String!, $name: String!) { - repository(name: $name, owner: $owner) { - id - isFork - defaultBranchRef { - name - } - } - }""", - owner=name_with_owner["owner"], - name=name_with_owner["name"], - )["data"]["repository"] - - return { - "name_with_owner": name_with_owner, - "id": repo["id"], - "is_fork": repo["isFork"], - "default_branch": repo["defaultBranchRef"]["name"], - } - - -RE_PR_URL = re.compile( - r"^https://(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/pull/(?P[0-9]+)/?$" -) - -GitHubPullRequestParams = TypedDict( - "GitHubPullRequestParams", - { - "github_url": str, - "owner": str, - "name": str, - "number": int, - }, -) - - -def parse_pull_request( - pull_request: str, - *, - sh: Optional[ghstack.shell.Shell] = None, - remote_name: Optional[str] = None, -) -> GitHubPullRequestParams: - m = RE_PR_URL.match(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", remote_name) - # Do not pass the shell to avoid infinite loop - try: - return parse_pull_request(remote_url + "/pull/" + pull_request) - except RuntimeError: - # Fall back on original error message - pass - raise RuntimeError("Did not understand PR argument. PR must be URL") - - github_url = m.group("github_url") - owner = m.group("owner") - name = m.group("name") - number = int(m.group("number")) - return {"github_url": github_url, "owner": owner, "name": name, "number": number} diff --git a/src/ghstack/ghstack/gpg_sign.py b/src/ghstack/ghstack/gpg_sign.py deleted file mode 100644 index f4c01f2..0000000 --- a/src/ghstack/ghstack/gpg_sign.py +++ /dev/null @@ -1,41 +0,0 @@ -"""This module determines if the commits need to be signed. -We need to do this manually, because ghstack uses commit-tree instead of commit. -commit-tree command doesn't pick up commit.gpgsign git config - -The porcelain git behavior w.r.t. signing is - -when both `commit.gpgsign` and `user.signingkey` are set, the commit is signed -when only `commit.gpgsign` is true, git errors out - -This module will retain this behavior: -We will attempt to sign as long as `commit.gpgsign` is true. -If not key is configure, error will occur -""" - -from typing import Tuple, Union - -import ghstack.shell - -_should_sign = None - - -def gpg_args_if_necessary( - shell: ghstack.shell.Shell = ghstack.shell.Shell(), -) -> Union[Tuple[str], Tuple[()]]: - global _should_sign - # cache the config result - if _should_sign is None: - # If the config is not set, we get exit 1 - 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 ( - "yes", - "on", - "true", - "1", - ) - except: - _should_sign = False - - return ("-S",) if _should_sign else () diff --git a/src/ghstack/ghstack/land.py b/src/ghstack/ghstack/land.py deleted file mode 100644 index 5e411fe..0000000 --- a/src/ghstack/ghstack/land.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import re -from typing import List, Optional, Tuple - -import ghstack.git -import ghstack.github -import ghstack.github_utils -import ghstack.shell -from ghstack.diff import PullRequestResolved -from ghstack.types import GitCommitHash - - -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( - """ - query ($owner: String!, $name: String!, $number: Int!) { - repository(name: $name, owner: $owner) { - pullRequest(number: $number) { - headRefName - closed - } - } - } - """, - owner=owner, - name=name, - number=number, - ) - pr = pr_result["data"]["repository"]["pullRequest"] - head_ref = pr["headRefName"] - closed = pr["closed"] - assert isinstance(head_ref, str) - orig_ref = re.sub(r"/head$", "/orig", head_ref) - if orig_ref == head_ref: - raise RuntimeError( - "The ref {} doesn't look like a ghstack reference".format(head_ref) - ) - return orig_ref, closed - - -def main( - pull_request: str, - remote_name: str, - github: ghstack.github.GitHubEndpoint, - sh: ghstack.shell.Shell, - github_url: str, - *, - force: bool = False, - repo_default_branch_opt: Optional[str] = None, -) -> None: - - # We land the entire stack pointed to by a URL. - # Local state is ignored; PR is source of truth - # 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( - pull_request, sh=sh, remote_name=remote_name - ) - default_branch = repo_default_branch_opt - if default_branch is None: - default_branch = 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"] - - needs_force = False - try: - protection = github.get( - f"repos/{params['owner']}/{params['name']}/branches/{default_branch}/protection" - ) - if not protection["allow_force_pushes"]["enabled"]: - raise RuntimeError( - """\ -Default branch {default_branch} is protected, and doesn't allow force pushes. -ghstack land does not work. You will not be able to land your ghstack; please -resubmit your PRs using the normal pull request flow. - -See https://github.com/ezyang/ghstack/issues/50 for more details, or -to complain to the ghstack authors.""" - ) - else: - needs_force = True - except ghstack.github.NotFoundError: - pass - - orig_ref, closed = lookup_pr_to_orig_ref_and_closed( - github, - owner=params["owner"], - name=params["name"], - number=params["number"], - ) - - if closed: - raise RuntimeError("PR is already closed, cannot land it!") - - if sh is None: - # Use CWD - sh = ghstack.shell.Shell() - - # Get up-to-date - sh.git("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) - ) - - # 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), - github_url=github_url, - ) - - # Switch working copy - try: - prev_ref = sh.git("symbolic-ref", "--short", "HEAD") - except RuntimeError: - prev_ref = sh.git("rev-parse", "HEAD") - - # If this fails, we don't have to reset - sh.git("checkout", f"{remote_name}/{default_branch}") - - try: - # Compute the metadata for each commit - stack_orig_refs: List[Tuple[str, PullRequestResolved]] = [] - for s in stack: - pr_resolved = s.pull_request_resolved - # 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( - github, - owner=pr_resolved.owner, - name=pr_resolved.repo, - number=pr_resolved.number, - ) - if closed and not force: - continue - stack_orig_refs.append((ref, pr_resolved)) - - # OK, actually do the land now - for orig_ref, _ in stack_orig_refs: - try: - sh.git("cherry-pick", f"{remote_name}/{orig_ref}") - except BaseException: - sh.git("cherry-pick", "--abort") - raise - - # All good! Push! - maybe_force_arg = [] - if needs_force: - maybe_force_arg = ["--force-with-lease"] - sh.git( - "push", *maybe_force_arg, remote_name, f"HEAD:refs/heads/{default_branch}" - ) - - # Advance base to head to "close" the PR for all PRs. - # This happens after the cherry-pick and push, because the cherry-picks - # can fail (merge conflict) and the push can also fail (race condition) - - # TODO: It might be helpful to advance orig to reflect the true - # state of upstream at the time we are doing the land, and then - # directly *merge* head into base, so that the PR accurately - # reflects what we ACTUALLY merged to master, as opposed to - # this synthetic thing I'm doing right now just to make it look - # like the PR got closed - - for orig_ref, pr_resolved in stack_orig_refs: - # 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( - "push", remote_name, f"{remote_name}/{head_ref}:refs/heads/{base_ref}" - ) - github.notify_merged(pr_resolved) - - # Delete the branches - for orig_ref, _ in stack_orig_refs: - # TODO: regex here so janky - 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) - 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) - except RuntimeError: - # Whatever, keep going - logging.warning("Failed to delete branch, continuing", exc_info=True) - - finally: - sh.git("checkout", prev_ref) diff --git a/src/ghstack/ghstack/logs.py b/src/ghstack/ghstack/logs.py deleted file mode 100644 index ec7af4c..0000000 --- a/src/ghstack/ghstack/logs.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 - -import contextlib -import datetime -import functools -import logging -import os -import re -import shutil -import subprocess -import sys -import uuid -from typing import Dict, Iterator, Optional - -DATETIME_FORMAT = "%Y-%m-%d_%Hh%Mm%Ss" - - -RE_LOG_DIRNAME = re.compile( - r"(\d{4}-\d\d-\d\d_\d\dh\d\dm\d\ds)_" r"[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}" -) - - -class Formatter(logging.Formatter): - redactions: Dict[str, str] - - def __init__(self, fmt: Optional[str] = None, datefmt: Optional[str] = None): - super().__init__(fmt, datefmt) - self.redactions = {} - - # Remove sensitive information from URLs - def _filter(self, s: str) -> str: - s = re.sub(r":\/\/(.*?)\@", r"://:@", s) - for needle, replace in self.redactions.items(): - s = s.replace(needle, replace) - return s - - def formatMessage(self, record: logging.LogRecord) -> str: - if record.levelno == logging.INFO or record.levelno == logging.DEBUG: - # Log INFO/DEBUG without any adornment - return record.getMessage() - else: - # I'm not sure why, but formatMessage doesn't show up - # even though it's in the typeshed for Python >3 - return super().formatMessage(record) # type: ignore - - def format(self, record: logging.LogRecord) -> str: - return self._filter(super().format(record)) - - # Redact specific strings; e.g., authorization tokens. This won't - # retroactively redact stuff you've already leaked, so make sure - # you redact things as soon as possible - def redact(self, needle: str, replace: str = "") -> None: - # Don't redact empty strings; this will lead to something - # that looks like str... - if needle == "": - return - self.redactions[needle] = replace - - -formatter = Formatter(fmt="%(levelname)s: %(message)s", datefmt="") - - -@contextlib.contextmanager -def manager(*, debug: bool = False) -> Iterator[None]: - # TCB code to setup logging. If a failure starts here we won't - # be able to save the user in a reasonable way. - - # Logging structure: there is one logger (the root logger) - # and in processes all events. There are two handlers: - # stderr (INFO) and file handler (DEBUG). - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - - console_handler = logging.StreamHandler() - if debug: - console_handler.setLevel(logging.DEBUG) - else: - console_handler.setLevel(logging.INFO) - console_handler.setFormatter(formatter) - root_logger.addHandler(console_handler) - - log_file = os.path.join(run_dir(), "ghstack.log") - - file_handler = logging.FileHandler(log_file) - # TODO: Hypothetically, it is better if we log the timestamp. - # But I personally feel the timestamps gunk up the log info - # for not much benefit (since we're not really going to be - # in the business of debugging performance bugs, for which - # timestamps would really be helpful.) Perhaps reconsider - # at some point based on how useful this information actually is. - # - # If you ever switch this, make sure to preserve redaction - # logic... - file_handler.setFormatter(formatter) - # file_handler.setFormatter(logging.Formatter( - # fmt="[%(asctime)s] [%(levelname)8s] %(message)s")) - root_logger.addHandler(file_handler) - - record_argv() - - try: - # Do logging rotation - rotate() - - yield - - except Exception as e: - logging.exception("Fatal exception") - record_exception(e) - sys.exit(1) - - -@functools.lru_cache() -def base_dir() -> str: - # Don't use shell here as we are not allowed to log yet! - try: - meta_dir = subprocess.run( - ("git", "rev-parse", "--git-dir"), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=True, - encoding="utf-8", - ).stdout.rstrip() - except subprocess.CalledProcessError: - meta_dir = os.path.join( - subprocess.run( - ("hg", "root"), stdout=subprocess.PIPE, encoding="utf-8", check=True - ).stdout.rstrip(), - ".hg", - ) - - base_dir = os.path.join(meta_dir, "ghstack", "log") - - try: - os.makedirs(base_dir) - except FileExistsError: - pass - - return base_dir - - -# Naughty, "run it once and save" memoizing -@functools.lru_cache() -def run_dir() -> str: - # NB: respects timezone - cur_dir = os.path.join( - base_dir(), - "{}_{}".format(datetime.datetime.now().strftime(DATETIME_FORMAT), uuid.uuid1()), - ) - - try: - os.makedirs(cur_dir) - except FileExistsError: - pass - - return cur_dir - - -def record_exception(e: BaseException) -> None: - with open(os.path.join(run_dir(), "exception"), "w") as f: - f.write(type(e).__name__) - - -@functools.lru_cache() -def record_argv() -> None: - with open(os.path.join(run_dir(), "argv"), "w") as f: - f.write(subprocess.list2cmdline(sys.argv)) - - -def record_status(status: str) -> None: - with open(os.path.join(run_dir(), "status"), "w") as f: - f.write(status) - - -def rotate() -> None: - log_base = base_dir() - old_logs = os.listdir(log_base) - old_logs.sort(reverse=True) - for stale_log in old_logs[1000:]: - # Sanity check that it looks like a log - assert RE_LOG_DIRNAME.fullmatch(stale_log) - shutil.rmtree(os.path.join(log_base, stale_log)) diff --git a/src/ghstack/ghstack/py.typed b/src/ghstack/ghstack/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/src/ghstack/ghstack/rage.py b/src/ghstack/ghstack/rage.py deleted file mode 100644 index d0c9fe8..0000000 --- a/src/ghstack/ghstack/rage.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import os -import tempfile -from typing import Dict, NewType - -import ghstack -import ghstack.logs - -RawIndex = NewType("RawIndex", int) -FilteredIndex = NewType("FilteredIndex", int) - - -def get_argv(log_dir: str) -> str: - argv = "Unknown" - argv_fn = os.path.join(log_dir, "argv") - if os.path.exists(argv_fn): - with open(argv_fn, "r") as f: - argv = f.read().rstrip() - return argv - - -def get_status(log_dir: str) -> str: - status = "" - status_fn = os.path.join(log_dir, "status") - if os.path.exists(status_fn): - with open(status_fn, "r") as f: - status = f.read().rstrip() - return status - - -def main(latest: bool = False) -> None: - - log_base = ghstack.logs.base_dir() - logs = os.listdir(log_base) - logs.sort(reverse=True) - - filtered_mapping: Dict[FilteredIndex, RawIndex] = {} - - selected_index: FilteredIndex = FilteredIndex(0) - next_index: FilteredIndex = FilteredIndex(0) - if not latest: - print("Which invocation would you like to report?") - print() - for i, fn in enumerate(logs): - if next_index > 10: - break - - raw_index = RawIndex(i) - log_dir = os.path.join(log_base, fn) - - # Filter out rage - # NB: This doesn't have to be 100% sound; just need to be - # enough to good enough to filter out the majority of cases - argv = get_argv(log_dir) - argv_list = argv.split() - - if len(argv_list) >= 2 and argv_list[1] == "rage": - continue - - if len(argv_list) >= 1: - argv_list[0] = os.path.basename(argv_list[0]) - - argv = " ".join(argv_list) - - status = get_status(log_dir) - if status: - at_status = " at {}".format(status) - else: - at_status = "" - - cur_index = next_index - next_index = FilteredIndex(next_index + 1) - - filtered_mapping[cur_index] = raw_index - - m = ghstack.logs.RE_LOG_DIRNAME.fullmatch(fn) - if m: - date = ( - datetime.datetime.strptime(m.group(1), ghstack.logs.DATETIME_FORMAT) - .astimezone(tz=None) - .strftime("%a %b %d %H:%M:%S %Z") - ) - else: - date = "Unknown" - exception = "Succeeded" - exception_fn = os.path.join(log_base, fn, "exception") - if os.path.exists(exception_fn): - with open(exception_fn, "r") as f: - exception = "Failed with: " + f.read().rstrip() - - print( - "{:<5} {} [{}] {}{}".format( - "[{}].".format(cur_index), date, argv, exception, at_status - ) - ) - print() - selected_index = FilteredIndex( - int(input("(input individual number, for example 1 or 2)\n")) - ) - - log_dir = os.path.join(log_base, logs[filtered_mapping[selected_index]]) - - print() - print("Writing report, please wait...") - with tempfile.NamedTemporaryFile( - mode="w", suffix=".log", prefix="ghstack", delete=False - ) as g: - g.write("version: {}\n".format(ghstack.__version__)) - g.write("command: {}\n".format(get_argv(log_dir))) - g.write("status: {}\n".format(get_status(log_dir))) - g.write("\n") - log_fn = os.path.join(log_dir, "ghstack.log") - if os.path.exists(log_fn): - with open(log_fn) as log: - g.write(log.read()) - - print("=> Report written to {}".format(g.name)) - print("Please include this log with your bug report!") diff --git a/src/ghstack/ghstack/shell.py b/src/ghstack/ghstack/shell.py deleted file mode 100644 index 91d1f6a..0000000 --- a/src/ghstack/ghstack/shell.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import logging -import os -import shlex -import subprocess -import sys -from typing import Any, Dict, IO, Optional, overload, Sequence, Tuple, TypeVar, Union - -# Shell commands generally return str, but with exitcode=True -# they return a bool, and if stdout is piped straight to sys.stdout -# they return None. -_SHELL_RET = Union[bool, str, None] - - -_HANDLE = Union[None, int, IO[Any]] - - -def log_command(args: Sequence[str]) -> None: - """ - Given a command, print it in a both machine and human readable way. - - Args: - *args: the list of command line arguments you want to run - env: the dictionary of environment variable settings for the command - """ - cmd = " ".join(shlex.quote(arg) for arg in args) - logging.info("$ " + cmd) - - -K = TypeVar("K") - - -V = TypeVar("V") - - -def merge_dicts(x: Dict[K, V], y: Dict[K, V]) -> Dict[K, V]: - z = x.copy() - z.update(y) - return z - - -class Shell(object): - """ - An object representing a shell (e.g., the bash prompt in your - terminal), maintaining a concept of current working directory, and - also the necessary accoutrements for testing. - """ - - # Current working directory of shell. - cwd: str - - # Whether or not to suppress printing of command executed. - quiet: bool - - # Whether or not shell is in testing mode; some commands are made - # more deterministic in this case. - testing: bool - - # The current Unix timestamp. Only used during testing mode. - testing_time: int - - def __init__( - self, quiet: bool = False, cwd: Optional[str] = None, testing: bool = False - ): - """ - Args: - cwd: Current working directory of the shell. Pass None to - initialize to the current cwd of the current process. - quiet: If True, suppress printing out the command executed - by the shell. By default, we print out commands for ease - of debugging. Quiet is most useful for non-mutating - shell commands. - testing: If True, operate in testing mode. Testing mode - enables features which make the outputs of commands more - deterministic; e.g., it sets a number of environment - variables for Git. - """ - self.cwd = cwd if cwd else os.getcwd() - self.quiet = quiet - 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: - """ - Run a command specified by args, and return string representing - the stdout of the run command, raising an error if exit code - was nonzero (unless exitcode kwarg is specified; see below). - - Args: - *args: the list of command line arguments to run - env: any extra environment variables to set when running the - command. Environment variables set this way are ADDITIVE - (unlike subprocess default) - stderr: where to pipe stderr; by default, we pipe it straight - to this process's stderr - input: string value to pass stdin. This is mutually exclusive - with stdin - stdin: where to pipe stdin from. This is mutually exclusive - with input - stdout: where to pipe stdout; by default, we capture the stdout - and return it - exitcode: if True, return a bool rather than string, specifying - whether or not the process successfully returned with exit - code 0. We never raise an exception when this is True. - """ - assert not (stdin and input) - if tick: - self.test_tick() - if input: - stdin = subprocess.PIPE - if not self.quiet: - log_command(args) - if env is not None: - env = merge_dicts(dict(os.environ), env) - - # The things we do for logging... - # - # - I didn't make a PTY, so programs are going to give - # output assuming there isn't a terminal at the other - # end. This is less nice for direct terminal use, but - # it's better for logging (since we get to dispense - # with the control codes). - # - # - We assume line buffering. This is kind of silly but - # we need to assume *some* sort of buffering with the - # stream API. - - async def process_stream( - proc_stream: asyncio.StreamReader, setting: _HANDLE, default_stream: IO[str] - ) -> bytes: - output = [] - while True: - try: - line = await proc_stream.readuntil() - except asyncio.LimitOverrunError as e: - line = await proc_stream.readexactly(e.consumed) - except asyncio.IncompleteReadError as e: - line = e.partial - if not line: - break - output.append(line) - if setting == subprocess.PIPE: - pass - elif setting == subprocess.STDOUT: - sys.stdout.buffer.write(line) - elif isinstance(setting, int): - os.write(setting, line) - elif setting is None: - # Sigh. See https://stackoverflow.com/questions/55681488/python-3-write-binary-to-stdout-respecting-buffering - default_stream.write(line.decode("utf-8")) - else: - # NB: don't use setting.write directly, that will - # not properly handle binary. This gives us - # "parity" with the normal subprocess implementation - os.write(setting.fileno(), line) - return b"".join(output) - - async def feed_input(stdin_writer: Optional[asyncio.StreamWriter]) -> None: - if stdin_writer is None: - return - if not input: - return - stdin_writer.write(input.encode("utf-8")) - await stdin_writer.drain() - stdin_writer.close() - - async def run() -> Tuple[int, bytes, bytes]: - proc = await asyncio.create_subprocess_exec( - *args, - stdin=stdin, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=self.cwd, - env=env, - ) - assert proc.stdout is not None - assert proc.stderr is not None - _, out, err, _ = await asyncio.gather( - feed_input(proc.stdin), - process_stream(proc.stdout, stdout, sys.stdout), - process_stream(proc.stderr, stderr, sys.stderr), - proc.wait(), - ) - assert proc.returncode is not None - return (proc.returncode, out, err) - - loop = asyncio.get_event_loop() - returncode, out, err = loop.run_until_complete(run()) - - def decode(b: bytes) -> str: - return ( - b.decode(errors="backslashreplace") - .replace("\0", "\\0") - .replace("\r\n", "\n") - ) - - # NB: Not debug; we always want to show this to user. - if err: - logging.debug("# stderr:\n" + decode(err)) - if out: - logging.debug(("# stdout:\n" if err else "") + decode(out)) - - if exitcode: - logging.debug("Exit code: {}".format(returncode)) - return returncode == 0 - if returncode != 0: - raise RuntimeError( - "{} failed with exit code {}".format(" ".join(args), returncode) - ) - - if stdout == subprocess.PIPE: - return out.decode() # do a strict decode for actual return - else: - return None - - def _maybe_rstrip(self, s: _SHELL_RET) -> _SHELL_RET: - if isinstance(s, str): - return s.rstrip() - else: - return s - - @overload # noqa: F811 - def git(self, *args: str) -> str: ... - - @overload # noqa: F811 - def git(self, *args: str, input: str) -> str: ... - - @overload # noqa: F811 - def git(self, *args: str, input: str, env: Dict[str, str]) -> str: ... - - @overload # noqa: F811 - def git(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... - - 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() - """ - env = kwargs.setdefault("env", {}) - # For git hooks to detect execution inside ghstack - env.setdefault("GHSTACK", "1") - # For dealing with https://github.com/ezyang/ghstack/issues/174 - env.setdefault( - "GIT_TERMINAL_PROMPT", os.environ.get("GIT_TERMINAL_PROMPT", "0") - ) - # Some envvars to make things a little more script mode nice - if self.testing: - env.setdefault("EDITOR", ":") - env.setdefault("GIT_MERGE_AUTOEDIT", "no") - env.setdefault("LANG", "C") - env.setdefault("LC_ALL", "C") - env.setdefault("PAGER", "cat") - env.setdefault("TZ", "UTC") - env.setdefault("TERM", "dumb") - # These are important so we get deterministic commit times - env.setdefault("GIT_AUTHOR_EMAIL", "author@example.com") - env.setdefault("GIT_AUTHOR_NAME", "A U Thor") - env.setdefault("GIT_COMMITTER_EMAIL", "committer@example.com") - env.setdefault("GIT_COMMITTER_NAME", "C O Mitter") - env.setdefault("GIT_COMMITTER_DATE", "{} -0700".format(self.testing_time)) - env.setdefault("GIT_AUTHOR_DATE", "{} -0700".format(self.testing_time)) - if "stderr" not in kwargs: - kwargs["stderr"] = subprocess.PIPE - - return self._maybe_rstrip(self.sh(*(("git",) + args), **kwargs)) - - @overload # noqa: F811 - def hg(self, *args: str) -> str: ... - - @overload # noqa: F811 - def hg(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._maybe_rstrip(self.sh(*(("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() - """ - - kwargs.setdefault("stdout", sys.stderr) - - return self._maybe_rstrip(self.sh(*(("jf",) + args), **kwargs)) - - def test_tick(self) -> None: - """ - Increase the current time. Useful when testing is True. - """ - self.testing_time += 60 - - def open(self, fn: str, mode: str) -> IO[Any]: - """ - Open a file, relative to the current working directory. - - Args: - fn: filename to open - mode: mode to open the file as - """ - return open(os.path.join(self.cwd, fn), mode) - - def cd(self, d: str) -> None: - """ - Change the current working directory. - - Args: - d: directory to change to - """ - self.cwd = os.path.join(self.cwd, d) diff --git a/src/ghstack/ghstack/status.py b/src/ghstack/ghstack/status.py deleted file mode 100644 index 313f5bf..0000000 --- a/src/ghstack/ghstack/status.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import asyncio -import logging -import re - -import aiohttp -from typing_extensions import TypedDict - -import ghstack.circleci -import ghstack.github -import ghstack.github_utils - -RE_CIRCLECI_URL = re.compile(r"^https://circleci.com/gh/pytorch/pytorch/([0-9]+)") - - -def strip_sccache(x: str) -> str: - sccache_marker = "=================== sccache compilation log ===================" - marker_pos = x.rfind(sccache_marker) - newline_before_marker_pos = x.rfind("\n", 0, marker_pos) - return x[:newline_before_marker_pos] - - -async def main( - pull_request: str, # noqa: C901 - github: ghstack.github.GitHubEndpoint, - circleci: ghstack.circleci.CircleCIEndpoint, -) -> None: - - # Game plan: - # 1. Query GitHub to find out what the current statuses are - # (TODO: if we got rate limited we'll miss stuff) - # 2. For each status in parallel: - # a. Query CircleCI for job status - # b. (Future work) Query output_url to get log information - # (it's gzip'ed) - # - # For now: - # - Print if the job actually ran, or was skipped - # - Easy way to determine: check if "Should run job after - # checkout" is last step - # - I inspected circleci.get('project/github/pytorch/pytorch/1773555') - # to see if there were other options, there did not appear - # 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) - - ContextPayload = TypedDict( - "ContextPayload", - { - "context": str, - "state": str, - "targetUrl": str, - }, - ) - r = github.graphql( - """ - query ($name: String!, $owner: String!, $number: Int!) { - repository(name: $name, owner: $owner) { - pullRequest(number: $number) { - commits(last: 1) { - nodes { - commit { - status { - contexts { - context - state - targetUrl - } - } - } - } - } - } - } - } - """, - **params, - ) - contexts = r["data"]["repository"]["pullRequest"]["commits"]["nodes"][0]["commit"][ - "status" - ]["contexts"] - - async def process_context(context: ContextPayload) -> str: - text = "" - if "circleci" in context["context"]: - m = RE_CIRCLECI_URL.match(context["targetUrl"]) - if not m: - logging.warning( - "Malformed CircleCI URL {}".format(context["targetUrl"]) - ) - return "INTERNAL ERROR {}".format(context["context"]) - buildid = m.group(1) - r = await circleci.get( - "project/github/{name}/{owner}/{buildid}".format( - buildid=buildid, **params - ) - ) - if context["state"] not in {"SUCCESS", "PENDING"}: - state = context["state"] - else: - if r["failed"]: - state = "FAILURE" - elif r["canceled"]: - state = "CANCELED" - elif "Should Run Job" in r["steps"][-1]["name"]: - state = "SKIPPED" - else: - state = "SUCCESS" - if state == "FAILURE": - async with aiohttp.request( - "get", r["steps"][-1]["actions"][-1]["output_url"] - ) as resp: - log_json = await resp.json() - buf = [] - for e in log_json: - buf.append(e["message"]) - text = "\n" + strip_sccache("\n".join(buf)) - text = text[-1500:] - else: - state = context["state"] - - if state == "SUCCESS": - state = "✅" - elif state == "SKIPPED": - state = "❔" - elif state == "CANCELED": - state = "💜" - elif state == "PENDING": - state = "🚸" - elif state == "FAILURE": - state = "❌" - name = context["context"] - url = context["targetUrl"] - url = url.replace( - "?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link", - "", - ) - return "{} {} {}{}".format(state, name.ljust(70), url, text) - - results = await asyncio.gather( - *[asyncio.ensure_future(process_context(c)) for c in contexts] - ) - print("\n".join(sorted(results))) diff --git a/src/ghstack/ghstack/submit.py b/src/ghstack/ghstack/submit.py deleted file mode 100644 index 797d00f..0000000 --- a/src/ghstack/ghstack/submit.py +++ /dev/null @@ -1,1849 +0,0 @@ -#!/usr/bin/env python3 - -import dataclasses -import itertools -import logging -import os -import re -from dataclasses import dataclass -from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple - -import ghstack -import ghstack.git -import ghstack.github -import ghstack.github_utils -import ghstack.gpg_sign -import ghstack.logs -import ghstack.shell -import ghstack.trailers -from ghstack.types import GhNumber, GitCommitHash, GitHubNumber, GitHubRepositoryId - -# Either "base", "head" or "orig"; which of the ghstack generated -# branches this diff corresponds to -# For direct ghstack, either "next", "head" or "orig" -BranchKind = str - - -@dataclass(frozen=True) -class GhCommit: - commit_id: GitCommitHash - tree: str - - -# Commit can be None if this is a completely fresh PR -@dataclass -class GhBranch: - commit: Optional[GhCommit] = None - updated: bool = False - - def update(self, val: GhCommit) -> None: - self.commit = val - self.updated = True - - -@dataclass -class GhBranches: - # What Git commit hash we should push to what branch. - # The orig branch is populated later - orig: GhBranch = dataclasses.field(default_factory=GhBranch) - head: GhBranch = dataclasses.field(default_factory=GhBranch) - base: GhBranch = dataclasses.field(default_factory=GhBranch) - next: GhBranch = dataclasses.field(default_factory=GhBranch) - - def to_list(self) -> List[Tuple[GitCommitHash, BranchKind]]: - r = [] - if self.orig.updated: - assert self.orig.commit is not None - r.append((self.orig.commit.commit_id, "orig")) - if self.next.updated: - assert self.next.commit is not None - r.append((self.next.commit.commit_id, "next")) - if self.base.updated: - assert self.base.commit is not None - r.append((self.base.commit.commit_id, "base")) - if self.head.updated: - assert self.head.commit is not None - r.append((self.head.commit.commit_id, "head")) - return r - - def __iter__(self) -> Iterator[Tuple[GitCommitHash, BranchKind]]: - return iter(self.to_list()) - - def __bool__(self) -> bool: - return bool(self.to_list()) - - def clear(self) -> None: - self.orig.updated = False - self.head.updated = False - self.base.updated = False - self.next.updated = False - - -@dataclass(frozen=True) -class PreBranchState: - # NB: these do not necessarily coincide with head/base branches. - # In particular, in direct mode, the base commit will typically be - # another head branch, or the upstream main branch itself. - base_commit_id: GitCommitHash - head_commit_id: GitCommitHash - - -# Ya, sometimes we get carriage returns. Crazy right? -RE_STACK = re.compile(r"Stack.*:\r?\n(\* [^\r\n]+\r?\n)+") - - -# NB: This regex is fuzzy because the D1234567 identifier is typically -# linkified. -RE_DIFF_REV = re.compile(r"^Differential Revision:.+?(D[0-9]+)", re.MULTILINE) - - -# Suppose that you have submitted a commit to GitHub, and that commit's -# tree was AAA. The ghstack-source-id of your local commit after this -# submit is AAA. When you submit a new change on top of this, we check -# that the source id associated with your orig commit agrees with what's -# recorded in GitHub: this lets us know that you are "up-to-date" with -# what was stored on GitHub. Then, we update the commit message on your -# local commit to record a new ghstack-source-id and push it to orig. -# -# We must store this in the orig commit as we have no other mechanism of -# attaching information to a commit in question. We don't store this in -# the pull request body as there isn't really any need to do so. -RE_GHSTACK_SOURCE_ID = re.compile(r"^ghstack-source-id: (.+)\n?", re.MULTILINE) - - -# When we make a GitHub PR using --direct, we submit an extra comment which -# contains the links to the rest of the PRs in the stack. We don't put this -# inside the pull request body, because if you squash merge the PR, that body -# gets put into the commit message, but the stack information is just line -# noise and shouldn't go there. -# -# We can technically find the ghstack commit by querying GitHub API for all -# comments, but this is a more efficient way of getting it. -RE_GHSTACK_COMMENT_ID = re.compile(r"^ghstack-comment-id: (.+)\n?", re.MULTILINE) - - -# repo layout: -# - gh/username/23/head -- what we think GitHub's current tip for commit is -# - gh/username/23/base -- what we think base commit for commit is -# - gh/username/23/orig -- the "clean" commit history, i.e., what we're -# rebasing, what you'd like to cherry-pick (???) -# (Maybe this isn't necessary, because you can -# get the "whole" diff from GitHub? What about -# commit description?) -# -# -# In direct mode, there is no base branch, instead: -# -# - gh/username/23/next -- staging ground for commits that must exist -# for later PRs in the stack to merge against, but should not be shown -# for the PR itself (because that PR was not submitted) - - -def branch(username: str, ghnum: GhNumber, kind: BranchKind) -> GitCommitHash: - return GitCommitHash("gh/{}/{}/{}".format(username, ghnum, kind)) - - -def branch_base(username: str, ghnum: GhNumber) -> GitCommitHash: - return branch(username, ghnum, "base") - - -def branch_head(username: str, ghnum: GhNumber) -> GitCommitHash: - return branch(username, ghnum, "head") - - -def branch_orig(username: str, ghnum: GhNumber) -> GitCommitHash: - return branch(username, ghnum, "orig") - - -def branch_next(username: str, ghnum: GhNumber) -> GitCommitHash: - return branch(username, ghnum, "next") - - -RE_MENTION = re.compile(r"(? str: - return RE_MENTION.sub(r"\1", body) - - -STACK_HEADER = f"Stack from [ghstack](https://github.com/ezyang/ghstack/tree/{ghstack.__version__}) (oldest at bottom)" - - -def starts_with_bullet(body: str) -> bool: - """ - Returns True if the string in question begins with a Markdown - bullet list - """ - return bool(re.match(r"^[\s\t]*[*\-+][\s\t]+", body)) - - -@dataclass -class DiffWithGitHubMetadata: - diff: ghstack.diff.Diff - number: GitHubNumber - username: str - # Really ought not to be optional, but for BC reasons it might be - remote_source_id: Optional[str] - # Guaranteed to be set for --direct PRs - comment_id: Optional[int] - title: str - body: str - closed: bool - ghnum: GhNumber - pull_request_resolved: ghstack.diff.PullRequestResolved - head_ref: str - base_ref: str - - -# Metadata describing a diff we submitted to GitHub -@dataclass -class DiffMeta: - elab_diff: DiffWithGitHubMetadata - # The commit message to put on the orig commit - commit_msg: str - - push_branches: GhBranches - # A human-readable string like 'Created' which describes what - # happened to this pull request - what: str - - # The name of the branch that should be targeted - base: str - - @property - def pr_url(self) -> str: - return self.elab_diff.pull_request_resolved.url() - - @property - def title(self) -> str: - return self.elab_diff.title - - @property - def number(self) -> GitHubNumber: - return self.elab_diff.number - - @property - def body(self) -> str: - return self.elab_diff.body - - @property - def username(self) -> str: - return self.elab_diff.username - - @property - def ghnum(self) -> GhNumber: - return self.elab_diff.ghnum - - @property - def closed(self) -> bool: - return self.elab_diff.closed - - @property - def orig(self) -> GitCommitHash: - assert self.push_branches.orig.commit is not None - return self.push_branches.orig.commit.commit_id - - @property - def head(self) -> GitCommitHash: - assert self.push_branches.head.commit is not None - return self.push_branches.head.commit.commit_id - - @property - def next(self) -> GitCommitHash: - assert self.push_branches.next.commit is not None - return self.push_branches.next.commit.commit_id - - -def main(**kwargs: Any) -> List[DiffMeta]: - submitter = Submitter(**kwargs) - return submitter.run() - - -def all_branches(username: str, ghnum: GhNumber) -> Tuple[str, str, str]: - return ( - branch_base(username, ghnum), - branch_head(username, ghnum), - branch_orig(username, ghnum), - ) - - -def push_spec(commit: GitCommitHash, branch: str) -> str: - return "{}:refs/heads/{}".format(commit, branch) - - -@dataclass(frozen=True) -class Submitter: - """ - A class responsible for managing the environment associated - with submitting PRs at GitHub. - """ - - # --------------------------- - # Direct arguments to submit - - # Message describing the update to the stack that was done - msg: Optional[str] - - # GitHub username who is doing the submitting - username: str - - # Endpoint to access GitHub - github: ghstack.github.GitHubEndpoint - - # Clobber existing PR description with local commit message - update_fields: bool = False - - # Shell inside git checkout that we are submitting - sh: ghstack.shell.Shell = dataclasses.field(default_factory=ghstack.shell.Shell) - - # String used to describe the stack in question - stack_header: str = STACK_HEADER - - # Owner of the repository we are submitting to. Usually 'pytorch' - # Presents as repo_owner kwarg in main - repo_owner_opt: Optional[str] = None - - # Name of the repository we are submitting to. Usually 'pytorch' - # Presents as repo_name kwarg in main - repo_name_opt: Optional[str] = None - - # allow configuration to optionally define repo details - repo_is_fork_opt: Optional[str] = None - repo_id_opt: Optional[str] = None - repo_default_branch_opt: Optional[str] = None - - # Print only PR URL to stdout - short: bool = False - - # Force an update to GitHub, even if we think that your local copy - # is stale. - force: bool = False - - # Do not skip unchanged diffs - no_skip: bool = False - - # Create the PR in draft mode if it is going to be created (and not updated). - draft: bool = False - - # Github url (normally github.com) - github_url: str = "github.com" - - # Name of the upstream remote (normally origin) - remote_name: str = "origin" - - base_opt: Optional[str] = None - - revs: Sequence[str] = () - - # Controls rev parse behavior, whether or not to submit a stack - # of commits or only one commit individually - stack: bool = True - - # Check that invariants are upheld during execution - check_invariants: bool = False - - # Instead of merging into base branch, merge directly into the appropriate - # main or head branch. Change merge targets appropriately as PRs get - # merged. If None, infer whether or not the PR should be direct or not. - direct_opt: Optional[bool] = None - - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # Computed in post init - - # GraphQL ID of the repository - repo_id: GitHubRepositoryId = dataclasses.field(init=False) - - repo_owner: str = dataclasses.field(init=False) - - repo_name: str = dataclasses.field(init=False) - - base: str = dataclasses.field(init=False) - - direct: bool = dataclasses.field(init=False) - - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # Mutable state; TODO: remove me - - # List of input diffs which we ignored (i.e., treated as if they - # did not exist on the stack at all), because they were associated - # with a patch that contains no changes. GhNumber may be false - # if the diff was never associated with a PR. - ignored_diffs: List[Tuple[ghstack.diff.Diff, Optional[DiffWithGitHubMetadata]]] = ( - dataclasses.field(default_factory=list) - ) - - # Set of seen ghnums - seen_ghnums: Set[Tuple[str, GhNumber]] = dataclasses.field(default_factory=set) - - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # Post initialization - - def __post_init__(self) -> None: - repo = {} - if ( - self.repo_id_opt is not None - and self.repo_owner_opt is not None - and self.repo_name_opt is not None - and self.repo_is_fork_opt is not None - and self.repo_default_branch_opt is not None - ): - repo["is_fork"] = self.repo_is_fork_opt - repo["id"] = self.repo_id_opt - repo["default_branch"] = self.repo_default_branch_opt - repo["name_with_owner"] = {} - repo["name_with_owner"]["owner"] = self.repo_owner_opt - repo["name_with_owner"]["name"] = self.repo_name_opt - else: - # Network call in the constructor, help me father, for I have sinned - repo = ghstack.github_utils.get_github_repo_info( - github=self.github, - sh=self.sh, - repo_owner=self.repo_owner_opt, - repo_name=self.repo_name_opt, - github_url=self.github_url, - remote_name=self.remote_name, - ) - object.__setattr__(self, "repo_owner", repo["name_with_owner"]["owner"]) - object.__setattr__(self, "repo_name", repo["name_with_owner"]["name"]) - - if repo["is_fork"]: - raise RuntimeError( - "Cowardly refusing to upload diffs to a repository that is a " - "fork. ghstack expects '{}' of your Git checkout to point " - "to the upstream repository in question. If your checkout does " - "not comply, please either adjust your remotes (by editing " - ".git/config) or change the 'remote_name' field in your .ghstackrc " - "file to point to the correct remote. If this message is in " - "error, please register your complaint on GitHub issues (or edit " - "this line to delete the check above).".format(self.remote_name) - ) - object.__setattr__(self, "repo_id", repo["id"]) - if self.base_opt is not None: - default_branch = self.base_opt - else: - default_branch = repo["default_branch"] - - object.__setattr__(self, "base", default_branch) - - # Check if direct should be used, if the user didn't explicitly - # specify an option - direct = self.direct_opt - if direct is None: - direct_r = self.sh.git( - "cat-file", "-e", "HEAD:.github/ghstack_direct", exitcode=True - ) - assert isinstance(direct_r, bool) - direct = direct_r - - object.__setattr__(self, "direct", direct) - - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # The main algorithm - - def run(self) -> List[DiffMeta]: - self.fetch() - - commits_to_submit_and_boundary = self.parse_revs() - - commits_to_submit = [ - d for d in commits_to_submit_and_boundary if not d.boundary - ] - - # NB: A little bit of redundant parsing here, because we will re-parse - # commits that we had already parsed in commits_to_submit, and we will - # 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( - "rev-list", - "--boundary", - "--header", - "--topo-order", - # Get all commits reachable from HEAD... - "HEAD", - # ...as well as all the commits we are going to submit... - *[c.commit_id for c in commits_to_submit], - # ...but we don't need any commits that aren't draft - f"^{self.remote_name}/{self.base}", - ) - ) - - commits_to_rebase = [ - d for d in commits_to_rebase_and_boundary if not d.boundary - ] - - # NB: commits_to_rebase always contains all diffs to submit (because - # we always have to generate orig commits for submitted diffs.) - # However, commits_to_submit does not necessarily contain - # diffs_to_rebase. If you ask to submit only a prefix of your current - # stack, the suffix is not to be submitted, but it needs to be rebased - # (to, e.g., update the ghstack-source-id) - - commit_count = len(commits_to_submit) - - if commit_count == 0: - raise RuntimeError( - "There appears to be no commits to process, based on the revs you passed me." - ) - - # 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 - ) - - # 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] = {} - if self.check_invariants: - 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) - pre_branch_state_index[h.commit_id] = PreBranchState( - head_commit_id=GitCommitHash( - self.sh.git( - "rev-parse", f"{self.remote_name}/{ed.head_ref}" - ) - ), - base_commit_id=GitCommitHash( - self.sh.git( - "rev-parse", f"{self.remote_name}/{ed.base_ref}" - ) - ), - ) - - # NB: deduplicates - commit_index = { - h.commit_id: h - for h in itertools.chain( - commits_to_submit_and_boundary, commits_to_rebase_and_boundary - ) - } - diff_meta_index, rebase_index = self.prepare_updates( - commit_index, commits_to_submit, commits_to_rebase - ) - logging.debug("rebase_index = %s", rebase_index) - diffs_to_submit = [ - diff_meta_index[h.commit_id] - for h in commits_to_submit - if h.commit_id in diff_meta_index - ] - self.push_updates(diffs_to_submit) - if new_head := rebase_index.get( - old_head := GitCommitHash(self.sh.git("rev-parse", "HEAD")) - ): - self.sh.git("reset", "--soft", new_head) - # TODO: print out commit hashes for things we rebased but not accessible - # from HEAD - - if self.check_invariants: - 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( - h.commit_id, - new_orig, - pre_branch_state_index.get(h.commit_id), - ) - # 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( - "merge-base", "--is-ancestor", h.commit_id, old_head, exitcode=True - ): - assert new_head is not None - assert self.sh.git( - "merge-base", "--is-ancestor", new_orig, new_head, exitcode=True - ) - else: - if new_head is not None: - assert not self.sh.git( - "merge-base", - "--is-ancestor", - new_orig, - new_head, - exitcode=True, - ) - - # 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 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, - # because you only did minor changes to the commits below and - # you want to let the CI finish without those changes. - # See https://github.com/ezyang/ghstack/issues/165 - # - # 2. I want to submit a prefix of the stack, because I'm still working - # on the top of the stack and don't want to spam people with - # useless changes. See https://github.com/ezyang/ghstack/issues/101 - # - # If we use standard git log/rev-list style parsing, you get (2) by - # default because a single commit implies a reachability constraint. - # Specifying (1) is a bit inconvenient; you have to say something - # like `ghstack submit HEAD~..`. In particular, both (1) and (2) would like - # the meaning of `ghstack submit HEAD` to do different things (1 wants a single - # commit, whereas 2 wants everything reachable from the commit.) - # - # To resolve the ambiguity, we introduce a new command line argument - # --no-stack (analogous to the --stack argument on jf) which disables - # "stacky" behavior. With --no-stack, we only submit HEAD by default - # and you can also specify a specific commit to submit if you like - # (if this commit is not reachable from HEAD, we will tell you how - # to checkout the updated commit.) If you specify multiple commits, - # we will process each of them in turn. Ranges are not supported; use - # git rev-list to preprocess them into single commits first (in principle - # we could support this, but it would require determining if a REV was - # a range versus a commit, as different handling would be necessary - # in each case.) - # - # Without --no-stack, we use standard git rev-list semantics. Some of the - # more advanced spellings can be counterintuitive, but `ghstack submit X` - # is equivalent to checking out X and then performing ghstack (and then - # restacking HEAD on top, if necessary), and you can say `X..Y` - # (exclusive-inclusive) to specify a specific range of commits (oddly, - # `X..` will do what you expect, but `..Y` will almost always be empty.) - # But I expect this to be fairly niche. - # - # In both cases, we support submitting multiple commits, because the set - # of commits you specify affects what rebasing we do, which is sometimes - # not conveniently done by calling ghstack multiple times. - - # Interestingly, the default is the same whether it is --stack or - # --no-stack - revs = ("HEAD",) if not self.revs else self.revs - - # In jf, we determine whether or not we should consider a diff by checking - # if it is draft or not (only draft commits can be posted). Git doesn't - # have a directly analogous concept, so we need some other strategy. A - # simple approach is to inspect the base branch in the upstream - # repository, and exclude all commits which are reachable from it. - # We don't want to blast ALL remote branches into the list here though; - # it's possible the draft commits were pushed to the remote repo for - # unrelated reasons, and we don't want to treat them as non-draft if - # this happens! - - commits_to_submit_and_boundary = [] - if self.stack: - # Easy case, make rev-list do the hard work - commits_to_submit_and_boundary.extend( - ghstack.git.split_header( - self.sh.git( - "rev-list", - "--header", - "--topo-order", - "--boundary", - *revs, - f"^{self.remote_name}/{self.base}", - ), - ) - ) - else: - # Hard case, need to query rev-list repeatedly - for rev in revs: - # We still do rev-list as it gets us the parent commits - r = ghstack.git.split_header( - self.sh.git( - "rev-list", - "--header", - "--topo-order", - "--boundary", - f"{rev}~..{rev}", - f"^{self.remote_name}/{self.base}", - ), - ) - if not r: - raise RuntimeError( - f"{r} doesn't seem to be a commit that can be submitted!" - ) - # NB: There may be duplicate commits that are - # boundary/not-boundary, but once we generate commits_to_submit - # there should not be any dupes if rev was not duped - # TODO: check no dupe revs, though actually it's harmless - commits_to_submit_and_boundary.extend(r) - - return commits_to_submit_and_boundary - - 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], - ) -> 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 - submit_set = set(h.commit_id for h in commits_to_submit) - diff_meta_index: Dict[GitCommitHash, DiffMeta] = {} - rebase_index: Dict[GitCommitHash, GitCommitHash] = {} - for commit in reversed(commits_to_rebase): - submit = commit.commit_id in submit_set - parents = commit.parents - if len(parents) != 1: - raise RuntimeError( - "The commit {} has {} parents, which makes my head explode. " - "`git rebase -i` your diffs into a stack, then try again.".format( - commit.commit_id, len(parents) - ) - ) - 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) - diff_meta = self.process_commit( - parent_commit, - parent_diff_meta, - diff, - ( - self.elaborate_diff(diff) - if diff.pull_request_resolved is not None - else None - ), - submit, - ) - 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 - - 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 - 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 - if commit.author_email is not None: - env["GIT_AUTHOR_EMAIL"] = commit.author_email - - new_orig = GitCommitHash( - self.sh.git( - "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), - "-p", - base_commit_id, - commit.tree, - input=commit_msg, - env=env, - ) - ) - - 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 - - return diff_meta_index, rebase_index - - def elaborate_diff( - self, diff: ghstack.diff.Diff, *, is_ghexport: bool = False - ) -> DiffWithGitHubMetadata: - """ - Query GitHub API for the current title, body and closed? status - of the pull request corresponding to a ghstack.diff.Diff. - """ - - assert diff.pull_request_resolved is not None - assert diff.pull_request_resolved.owner == self.repo_owner - 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: - 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. -""" - ) - - # TODO: Hmm, I'm not sure why this matches - m = re.match(r"gh/([^/]+)/([0-9]+)/head$", r["headRefName"]) - 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 - ) - ) - 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) - - # TODO: remote summary should be done earlier so we can use - # it to test if updates are necessary - - try: - rev_list = self.sh.git( - "rev-list", - "--max-count=1", - "--header", - self.remote_name + "/" + branch_orig(username, gh_number), - ) - except RuntimeError as e: - if r["closed"]: - raise RuntimeError( - f"Cannot ghstack a stack with closed PR #{number} 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." - ) from e - raise - remote_summary = ghstack.git.split_header(rev_list)[0] - m_remote_source_id = RE_GHSTACK_SOURCE_ID.search(remote_summary.commit_msg) - remote_source_id = m_remote_source_id.group(1) if m_remote_source_id else None - m_comment_id = RE_GHSTACK_COMMENT_ID.search(remote_summary.commit_msg) - comment_id = int(m_comment_id.group(1)) if m_comment_id else None - - return DiffWithGitHubMetadata( - diff=diff, - title=title, - body=pr_body, - closed=r["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"], - ) - - def process_commit( - self, - base: ghstack.git.CommitHeader, - base_diff_meta: Optional[DiffMeta], - diff: ghstack.diff.Diff, - elab_diff: Optional[DiffWithGitHubMetadata], - submit: bool, - ) -> Optional[DiffMeta]: - # Do not process poisoned commits - if "[ghstack-poisoned]" in diff.summary: - self._raise_poisoned() - - # Do not process closed commits - if elab_diff is not None and elab_diff.closed: - if self.direct: - self._raise_needs_rebase() - return None - - # Edge case: check if the commit is empty; if so skip submitting - if base.tree == diff.tree: - self._warn_empty(diff, elab_diff) - # Maybe it can just fall through here and make an empty PR fine - assert not self.direct, "empty commits with direct NYI" - 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() - self._sanity_check_ghnum(username, ghnum) - - # Create base/head commits if needed - push_branches, base_branch = self._create_non_orig_branches( - base, base_diff_meta, diff, elab_diff, username, ghnum, submit - ) - - # 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] - ) - push_branches.clear() - elab_diff = self._create_pull_request(diff, base_diff_meta, ghnum) - what = "Created" - new_pr = True - else: - if not push_branches: - what = "Skipped" - elif push_branches.head is None: - 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, - ) - - def _raise_poisoned(self) -> None: - raise RuntimeError( - """\ -This commit is poisoned: it is from a head or base branch--ghstack -cannot validly submit it. The most common situation for this to -happen is if you checked out the head branch of a pull request that was -previously submitted with ghstack (e.g., by using hub checkout). -Making modifications on the head branch is not supported; instead, -you should fetch the original commits in question by running: - -ghstack checkout $PR_URL - -Since we cannot proceed, ghstack will abort now. -""" - ) - - def _raise_needs_rebase(self) -> None: - raise RuntimeError( - """\ -ghstack --next requires all PRs in the stack to be open. One of your PRs -is closed (likely due to being merged). Please rebase to upstream and try again. -""" - ) - - def _warn_empty( - self, diff: ghstack.diff.Diff, elab_diff: Optional[DiffWithGitHubMetadata] - ) -> None: - self.ignored_diffs.append((diff, elab_diff)) - logging.warning( - "Skipping '{}', as the commit now has no changes".format(diff.title) - ) - - 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) - 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)) - - def _sanity_check_ghnum(self, username: str, ghnum: GhNumber) -> None: - if (username, ghnum) in self.seen_ghnums: - raise RuntimeError( - "Something very strange has happened: a commit for " - f"the gh/{username}/{ghnum} 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." - ) - self.seen_ghnums.add((username, ghnum)) - - def _update_source_id(self, summary: str, elab_diff: DiffWithGitHubMetadata) -> str: - m_local_source_id = RE_GHSTACK_SOURCE_ID.search(summary) - if m_local_source_id is None: - # This is for an already submitted PR, so there should - # already be a source id on it. But there isn't. - # For BC, just slap on a source ID. After BC is no longer - # needed, we can just error in this case; however, this - # situation is extremely likely to happen for preexisting - # stacks. - logging.warning( - "Local commit has no ghstack-source-id; assuming that it is " - "up-to-date with remote." - ) - summary = "{}\nghstack-source-id: {}".format( - summary, elab_diff.diff.source_id - ) - else: - local_source_id = m_local_source_id.group(1) - if elab_diff.remote_source_id is None: - # This should also be an error condition, but I suppose - # it can happen in the wild if a user had an aborted - # ghstack run, where they updated their head pointer to - # a copy with source IDs, but then we failed to push to - # orig. We should just go ahead and push in that case. - logging.warning( - "Remote commit has no ghstack-source-id; assuming that we are " - "up-to-date with remote." - ) - elif local_source_id != elab_diff.remote_source_id and not self.force: - logging.debug( - f"elab_diff.remote_source_id = {elab_diff.remote_source_id}" - ) - # TODO: have a 'ghstack pull' remediation for this case - raise RuntimeError( - "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 " - "{} and reapply them on top of an up-to-date commit from " - "GitHub.".format(local_source_id) - ) - summary = RE_GHSTACK_SOURCE_ID.sub( - "ghstack-source-id: {}\n".format(elab_diff.diff.source_id), summary - ) - return summary - - # NB: mutates GhBranch - 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) - ) - 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: - push_branches = GhBranches() - self._resolve_gh_branch("orig", push_branches.orig, username, ghnum) - self._resolve_gh_branch("head", push_branches.head, username, ghnum) - if self.direct: - self._resolve_gh_branch("next", push_branches.next, username, ghnum) - else: - self._resolve_gh_branch("base", push_branches.base, username, ghnum) - return push_branches - - def _create_non_orig_branches( - self, - base: ghstack.git.CommitHeader, - base_diff_meta: Optional[DiffMeta], - diff: ghstack.diff.Diff, - elab_diff: Optional[DiffWithGitHubMetadata], - username: str, - ghnum: GhNumber, - submit: bool, - ) -> Tuple[GhBranches, str]: - # How exactly do we submit a commit to GitHub? - # - # Here is the relevant state: - # - Local parent tree - # - Local commit tree - # - Remote base branch - # - Remote head branch - # - # Our job is to synchronize local with remote. Here are a few - # common situations: - # - # - Neither this commit nor any of the earlier commits were - # modified; everything is in sync. We want to do nothing in this - # case. - # - # - User updated top commit on stack, but none of the earlier commits. - # Here, we expect local parent tree to match remote base tree (BA), but - # local commit tree to mismatch remote head branch (A). We will push - # a new commit to head (A2), no merge necessary. - # - # BA - # \ - # A - A2 - # - # - User updated an earlier commit in the stack (it doesn't matter - # if the top commit is logically modified or not: it always counts as - # having been modified to resolve the merge.) We don't expect - # local parent tree to match remote base tree, so we must push a - # new base commit (BA2), and a merge commit (A2) on it. - # - # BA - BA2 - # \ \ - # A - A2 - # - # Notably, this must happen even if the local commit tree matches - # the remote head branch. A common situation this could occur is - # if we squash commits I and J into IJ (keeping J as the tree). - # Then for J we see: - # - # BJ - BJ2 - # \ \ - # J - BJ2 - # - # Where BJ contains I, but BJ2 does NOT contain I. The net result - # is the changes of I are included inside the BJ2 merge commit. - # - # First time submission proceeds similarly, except that we no longer - # need to create a parent pointer to the previous base/head. - # - # Note that, counterintuitively, the base of a diff has no - # relationship to the head of an earlier diff on the stack. This - # makes it possible to selectively only update one diff in a stack - # without updating any others. This also makes our handling uniform - # even if you rebase a commit backwards: you just see that the base - # is updated to also remove changes. - - if elab_diff is not None: - push_branches = self._resolve_gh_branches(username, ghnum) - else: - push_branches = GhBranches() - - # Initialize head arguments (as original head parent must come first - # in parents list) - head_args: List[str] = [] - if push_branches.head.commit is not None: - head_args.extend(("-p", push_branches.head.commit.commit_id)) - - # Create base commit if necessary - updated_base = False - if not self.direct: - base_branch = branch_base(username, ghnum) - if ( - push_branches.base.commit is None - or push_branches.base.commit.tree != base.tree - ): - # Base is not the same, perform base update - updated_base = True - base_args: List[str] = [] - if push_branches.base.commit is not None: - base_args.extend(("-p", push_branches.base.commit.commit_id)) - # We don't technically need to do this, but often tooling - # relies on pull requests being able to compute merge-base - # with the main branch. While the result you get here can be - # misleading (in particular, the merge-base will not - # 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( - "merge-base", base.commit_id, f"{self.remote_name}/{self.base}" - ) - if push_branches.base.commit is None or not self.sh.git( - "merge-base", - "--is-ancestor", - extra_base, - push_branches.base.commit.commit_id, - exitcode=True, - ): - base_args.extend(("-p", extra_base)) - new_base = GitCommitHash( - self.sh.git( - "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), - *base_args, - base.tree, - input="{} (base update)\n\n[ghstack-poisoned]".format(self.msg), - ) - ) - head_args.extend(("-p", new_base)) - push_branches.base.update(GhCommit(new_base, base.tree)) - else: - # So, there is some complication here. We're computing what base - # to use based on the local situation on the user diff stack, but - # the remote merge structure may disagree with our local - # situation. For example, suppose I have a commit stack A - B, - # and then I insert a new commit A - M - B between them; - # previously B would have been based on A, but now it is based - # on M. What should happen here? - # - # Here are the high level correctness conditions: - # - No force pushes (history must be preserved) - # - GitHub displays a diff which is equivalent to the original - # user diff - # - # It turns out the logic here is fine, and the only thing it - # chokes on is rebasing back in time on master branch (you can't - # go back in time on PR branches, so this is a moot point there.) - # The problem is suppose you have: - # - # A - B - C - # \ \ - # M2 M1 # M1 was cherry-picked onto A, becoming M2 - # - # In branch form, this becomes: - # - # A - B - C - # \ \ - # \ M1 - M2 - # \ / - # \-----/ - # - # However, the merge base for C and M2 will always be computed to - # be B, because B is an ancestor of both C and M2, and it always - # beets out A (which is an ancestor of B). This means that you - # will diff M2 against B, which will typically result in "remove - # changes from B" spuriously showing up on the PR. - # - # When heads are always monotonically moving forward in time, - # there is not any problem with progressively more complicated - # merge histories, because we always specify the "correct" base - # branch. For example, consider: - # - # A - B - # \ - # \- X - Y1 - # \ - # \- Y2 - # - # Where Y1 is cherry-picked off of X onto B directly. In branch - # form, this becomes: - # - # A - B - # \ - # \- X - Y1 - Y2 - # - # But we update the base branch to be B, so we correctly diff Y2 - # against B (where here, the tree for Y2 no longer incorporates - # the changes for X). - # - # What does NOT work in this situation is if you manually (outside - # of ghstack) retarget Y2 back at X; we will spuriously report - # that the diff X and Y2 removes the changes from X. If you use - # ghstack, however, we will do this: - # - # A - B - # \ - # \- X - Y1 - Y2 - Y3 - # - # Where here Y3 has restored the changes from X, so the diff from - # X to Y3 checks out. - # - # It turns out there are a subset of manipulations, for which it - # is always safe to change the target base commit from GitHub UI - # without pushing a new commit. Intuitively, the idea is that - # once you add a commit as a merge base, you can't take it back: - # we always consider that branch to have been "merged in". So - # you can effectively only ever insert new commits between - # pre-existing commits, but once a commit depends on another - # commit, that dependency must always exist. I'm still - # considering whether or not we should force push by default in - # this sort of situation. - # - # By the way, what happens if you reorder commits? You get this - # funny looking graph: - # - # A - B - # \ - # X - Y - Y2 - # \ \ - # \------- X2 - - # We never have to create a base commit, we read it out from - # the base - if base_diff_meta is not None: - # The base was submitted the normal way (merge base is either - # next or head) - # - # We can always use next, because if head is OK, head will have - # been advanced to next anyway - # - # TODO: I do not feel this can be None - if base_diff_meta.head is not None: - # TODO: This assert is sus, next may be ahed of head - assert base_diff_meta.next == base_diff_meta.head - new_base = base_diff_meta.next - - if base_diff_meta.next == base_diff_meta.head: - # use head - base_branch = branch_head( - base_diff_meta.username, base_diff_meta.ghnum - ) - else: - # use next - base_branch = branch_next( - base_diff_meta.username, base_diff_meta.ghnum - ) - else: - # TODO: test that there isn't a more recent ancestor - # such that this doesn't actually work - new_base = base.commit_id - - base_branch = GitCommitHash(self.base) - - # 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( - "merge-base", - "--is-ancestor", - new_base, - push_branches.next.commit.commit_id, - exitcode=True, - ): - new_base = None - - if new_base is not None: - updated_base = True - head_args.extend(("-p", new_base)) - - # Check head commit if necessary - if ( - push_branches.head.commit is None - or updated_base - or push_branches.head.commit.tree != diff.tree - ): - new_head = GitCommitHash( - self.sh.git( - "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), - *head_args, - diff.tree, - input="{}\n\n[ghstack-poisoned]".format(self.msg), - ) - ) - if self.direct: - # only update head branch if we're actually submitting - if submit: - push_branches.head.update(GhCommit(new_head, diff.tree)) - push_branches.next.update(GhCommit(new_head, diff.tree)) - else: - push_branches.head.update(GhCommit(new_head, diff.tree)) - - return push_branches, base_branch - - def _create_pull_request( - self, - diff: ghstack.diff.Diff, - base_diff_meta: Optional[DiffMeta], - ghnum: GhNumber, - ) -> DiffWithGitHubMetadata: - title, body = self._default_title_and_body(diff, None) - head_ref = branch_head(self.username, ghnum) - - if self.direct: - if base_diff_meta is None: - base_ref = self.base - else: - base_ref = branch_head(base_diff_meta.username, base_diff_meta.ghnum) - else: - base_ref = branch_base(self.username, ghnum) - - # Time to open the PR - # NB: GraphQL API does not support opening PRs - r = self.github.post( - "repos/{owner}/{repo}/pulls".format( - owner=self.repo_owner, repo=self.repo_name - ), - title=title, - head=head_ref, - base=base_ref, - body=body, - maintainer_can_modify=True, - draft=self.draft, - ) - number = r["number"] - - comment_id = None - if self.direct: - rc = self.github.post( - f"repos/{self.repo_owner}/{self.repo_name}/issues/{number}/comments", - body=f"{self.stack_header}:\n* (to be filled)", - ) - comment_id = rc["id"] - - logging.info("Opened PR #{}".format(number)) - - pull_request_resolved = ghstack.diff.PullRequestResolved( - owner=self.repo_owner, - repo=self.repo_name, - number=number, - github_url=self.github_url, - ) - - return DiffWithGitHubMetadata( - diff=diff, - number=number, - username=self.username, - remote_source_id=diff.source_id, # in sync - comment_id=comment_id, - title=title, - body=body, - closed=False, - ghnum=ghnum, - pull_request_resolved=pull_request_resolved, - head_ref=head_ref, - base_ref=base_ref, - ) - - def push_updates( - self, diffs_to_submit: List[DiffMeta], *, 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] = [] - - 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 - elif b == "base": - q = base_push_branches - else: - q = push_branches - q.append(push_spec(diff, branch(s.username, s.ghnum, b))) - # Careful! Don't push master. - # 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) - - for s in reversed(diffs_to_submit): - # NB: GraphQL API does not support modifying PRs - assert not s.closed - logging.info( - "# Updating https://{github_url}/{owner}/{repo}/pull/{number}".format( - github_url=self.github_url, - owner=self.repo_owner, - repo=self.repo_name, - number=s.number, - ) - ) - # TODO: don't update this if it doesn't need updating - base_kwargs = {} - if self.direct: - base_kwargs["base"] = s.base - else: - assert s.base == s.elab_diff.base_ref - stack_desc = self._format_stack(diffs_to_submit, s.number) - 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( - stack_desc, - s.body, - ), - title=s.title, - **base_kwargs, - ) - - 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, - ) - - # Report what happened - def format_url(s: DiffMeta) -> str: - return "https://{github_url}/{owner}/{repo}/pull/{number}".format( - github_url=self.github_url, - owner=self.repo_owner, - repo=self.repo_name, - number=s.number, - ) - - if self.short: - # Guarantee that the FIRST PR URL is the top of the stack - print("\n".join(format_url(s) for s in reversed(diffs_to_submit))) - return - - print() - print("# Summary of changes (ghstack {})".format(ghstack.__version__)) - print() - if diffs_to_submit: - for s in reversed(diffs_to_submit): - url = format_url(s) - print(" - {} {}".format(s.what, url)) - - print() - if import_help: - top_of_stack = diffs_to_submit[0] - - print("Meta employees can import your changes by running ") - print("(on a Meta machine):") - print() - print(" ghimport -s {}".format(format_url(top_of_stack))) - print() - print("If you want to work on this diff stack on another machine:") - print() - print(" ghstack checkout {}".format(format_url(top_of_stack))) - print("") - else: - print( - "No pull requests updated; all commits in your diff stack were empty!" - ) - - if self.ignored_diffs: - print() - print("FYI: I ignored the following commits, because they had no changes:") - print() - noop_pr = False - for d, elab_diff in reversed(self.ignored_diffs): - if elab_diff is None: - print(" - {} {}".format(d.oid[:8], d.title)) - else: - noop_pr = True - print( - " - {} {} (was previously submitted as PR #{})".format( - d.oid[:8], d.title, elab_diff.number - ) - ) - if noop_pr: - print() - print( - "I did NOT close or update PRs previously associated with these commits." - ) - - def check_invariants_for_diff( - self, - # the user diff is what the user actual sent us - user_commit_id: GitCommitHash, - orig_commit_id: GitCommitHash, - pre_branch_state: Optional[PreBranchState], - ) -> None: - def is_git_commit_hash(h: str) -> bool: - return re.match(r"[a-f0-9]{40}", h) is not None - - def assert_eq(a: Any, b: Any) -> None: - assert a == b, f"{a} != {b}" - - assert is_git_commit_hash(user_commit_id) - assert is_git_commit_hash(orig_commit_id) - if pre_branch_state: - assert is_git_commit_hash(pre_branch_state.head_commit_id) - assert is_git_commit_hash(pre_branch_state.base_commit_id) - - # 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) - ) - 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) - ) - assert_eq(orig_commit.commit_id, orig_commit_id) - assert not orig_commit.boundary - assert orig_parent_commit.boundary - - user_diff = ghstack.git.convert_header(user_commit, self.github_url) - orig_diff = ghstack.git.convert_header(orig_commit, self.github_url) - - # 1. Used same PR if it exists - if (pr := user_diff.pull_request_resolved) is not None: - assert_eq(pr, orig_diff.pull_request_resolved) - - # 2. Must have a PR after running - assert orig_diff.pull_request_resolved is not None - - # 3. We didn't corrupt the diff - assert_eq(user_commit.tree, orig_commit.tree) - assert_eq(user_parent_commit.tree, orig_parent_commit.tree) - - # 4. Orig diff has correct metadata - m = RE_GHSTACK_SOURCE_ID.search(orig_commit.commit_msg) - assert m is not None - assert_eq(m.group(1), orig_commit.tree) - - elaborated_orig_diff = 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}") - ) - assert_eq(head_commit.tree, user_commit.tree) - - base_ref = elaborated_orig_diff.base_ref - - if not self.direct: - assert_eq(base_ref, branch_base(self.username, elaborated_orig_diff.ghnum)) - else: - # TODO: assert the base is the head of the next branch, or main - pass - - (base_commit,) = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "-1", f"{self.remote_name}/{base_ref}") - ) - # TODO: tree equality may not hold for self.direct, figure out a - # related invariant - if not self.direct: - assert_eq(base_commit.tree, user_parent_commit.tree) - - # 6. Orig commit was correctly pushed - assert_eq( - orig_commit.commit_id, - GitCommitHash( - self.sh.git( - "rev-parse", - self.remote_name - + "/" - + branch_orig(self.username, elaborated_orig_diff.ghnum), - ) - ), - ) - - # 7. Branches are either unchanged, or parent (no force pushes) - # NB: head is always merged in as first parent - # NB: you could relax this into an ancestor check - if pre_branch_state: - assert pre_branch_state.head_commit_id in [ - head_commit.commit_id, - head_commit.parents[0], - ] - # The base branch can change if we changed base in direct mode - if not self.direct: - assert pre_branch_state.base_commit_id in [ - base_commit.commit_id, - *([base_commit.parents[0]] if base_commit.parents else []), - ] - else: - # Direct commit parent typically have base, as it will be the - # main branch - if not self.direct: - pass - # This is now set to the orig base - # assert not base_commit.parents - - # 8. Head branch is not malformed - assert self.sh.git( - "merge-base", - "--is-ancestor", - base_commit.commit_id, - head_commit.commit_id, - exitcode=True, - ) - - # 9. Head and base branches are correctly poisoned - assert "[ghstack-poisoned]" in head_commit.commit_msg - - # TODO: direct PR based on main are not poisoned base commit - if not self.direct: - assert "[ghstack-poisoned]" in base_commit.commit_msg - - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # Small helpers - - # TODO: do the tree formatting minigame - # Main things: - # - need to express some tree structure - # - want "as complete" a tree as possible; this may involve - # poking around the xrefs to find out all the other PRs - # involved in the stack - def _format_stack(self, diffs_to_submit: List[DiffMeta], number: int) -> str: - rows = [] - # NB: top is top of stack, opposite of update order - for s in diffs_to_submit: - if s.number == number: - rows.append(f"* __->__ #{s.number}") - else: - rows.append(f"* #{s.number}") - return self.stack_header + ":\n" + "\n".join(rows) + "\n" - - def _default_title_and_body( - self, diff: ghstack.diff.Diff, old_pr_body: Optional[str] - ) -> Tuple[str, str]: - """ - Compute what the default title and body of a newly opened pull - request would be, given the existing commit message. - - If you pass in the old PR body, we also preserve "Differential - Revision" information in the PR body. We only overwrite PR - body if you explicitly ask for it with --update-fields, but - it's good not to lose Phabricator diff assignment, so we special - case this. - """ - title = diff.title - extra = "" - if old_pr_body is not None: - # Look for tags we should preserve, and keep them - m = RE_DIFF_REV.search(old_pr_body) - if m: - extra = ( - "\n\nDifferential Revision: " - "[{phabdiff}]" - "(https://our.internmc.facebook.com/intern/diff/{phabdiff})" - ).format(phabdiff=m.group(1)) - commit_body = "".join(diff.summary.splitlines(True)[1:]).lstrip() - # Don't store ghstack-source-id in the PR body; it will become - # stale quickly - commit_body = RE_GHSTACK_SOURCE_ID.sub("", commit_body) - # Comment ID is not necessary; source of truth is orig commit - commit_body = RE_GHSTACK_COMMENT_ID.sub("", commit_body) - # Don't store Pull request in the PR body; it's - # unnecessary - commit_body = ghstack.diff.re_pull_request_resolved_w_sp(self.github_url).sub( - "", commit_body - ) - if self.direct: - pr_body = f"{commit_body}{extra}" - else: - if starts_with_bullet(commit_body): - commit_body = f"----\n\n{commit_body}" - pr_body = "{}:\n* (to be filled)\n\n{}{}".format( - self.stack_header, commit_body, extra - ) - return title, pr_body - - def _git_push(self, branches: Sequence[str], force: bool = False) -> None: - assert branches, "empty branches would push master, probably bad!" - try: - self.sh.git( - "push", - self.remote_name, - "--no-verify", - *(["--force"] if force else []), - *branches, - ) - except RuntimeError as e: - remote_url = self.sh.git("remote", "get-url", "--push", self.remote_name) - if remote_url.startswith("https://"): - raise RuntimeError( - "[E001] git push failed, probably because it asked for password " - "(scroll up to see original error). " - "Change your git URL to use SSH instead of HTTPS to enable passwordless push. " - "See https://github.com/ezyang/ghstack/wiki/E001 for more details." - ) from e - raise - self.github.push_hook(branches) - - -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" - ) - try: - hooks_path = sh.git( - "config", "--default", default_hooks_path, "--get", "core.hooksPath" - ) - hook_file = os.path.join(hooks_path, "pre-ghstack") - except Exception as e: - logging.warning(f"Pre ghstack hook failed: {e}") - return - - 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) diff --git a/src/ghstack/ghstack/test_prelude.py b/src/ghstack/ghstack/test_prelude.py deleted file mode 100644 index 1801e5a..0000000 --- a/src/ghstack/ghstack/test_prelude.py +++ /dev/null @@ -1,400 +0,0 @@ -import argparse -import atexit -import contextlib -import io -import os -import re -import shutil -import stat -import sys -import tempfile -from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union - -from expecttest import assert_expected_inline - -import ghstack.github -import ghstack.github_fake -import ghstack.github_utils -import ghstack.land -import ghstack.shell -import ghstack.submit -import ghstack.unlink -from ghstack.types import GitCommitHash - -__all__ = [ - "ghstack", - "init_test", - "commit", - "git", - "gh_submit", - "gh_land", - "gh_unlink", - "GitCommitHash", - "checkout", - "amend", - "commit", - "cherry_pick", - "dump_github", - "ok", - "is_direct", - "write_file_and_add", - "assert_expected_inline", - "assert_raises", - "assert_expected_raises_inline", - "assert_github_state", - "assert_eq", - "get_sh", - "get_upstream_sh", - "get_github", - "tick", - "captured_output", -] - -GH_KEEP_TMP = os.getenv("GH_KEEP_TMP") - - -@contextlib.contextmanager -def captured_output() -> Iterator[Tuple[io.StringIO, io.StringIO]]: - new_out, new_err = io.StringIO(), io.StringIO() - old_out, old_err = sys.stdout, sys.stderr - try: - sys.stdout, sys.stderr = new_out, new_err - yield sys.stdout, sys.stderr - finally: - sys.stdout, sys.stderr = old_out, old_err - - -def strip_trailing_whitespace(text: str) -> str: - return re.sub(r" +$", "", text, flags=re.MULTILINE) - - -def indent(text: str, prefix: str) -> str: - return "".join( - prefix + line if line.strip() else line for line in text.splitlines(True) - ) - - -def handle_remove_read_only(func: Callable[..., Any], path: str, exc_info: Any) -> None: - """ - Error handler for ``shutil.rmtree``. - - If the error is due to an access error (read only file), - it attempts to add write permission and then retries. - - If the error is for another reason, it re-raises the error. - - Usage : ``shutil.rmtree(path, onerror=onerror)`` - """ - - if not os.access(path, os.W_OK): - os.chmod(path, stat.S_IWUSR) - func(path) - else: - raise - - -class Context: - github: ghstack.github.GitHubEndpoint - upstream_sh: ghstack.shell.Shell - sh: ghstack.shell.Shell - direct: bool - - def __init__(self, direct: bool) -> None: - # Set up a "parent" repository with an empty initial commit that we'll operate on - upstream_dir = tempfile.mkdtemp() - self.upstream_sh = ghstack.shell.Shell(cwd=upstream_dir, testing=True) - self.github = ghstack.github_fake.FakeGitHubEndpoint(self.upstream_sh) - - local_dir = tempfile.mkdtemp() - self.sh = ghstack.shell.Shell(cwd=local_dir, testing=True) - self.sh.git("clone", upstream_dir, ".") - self.direct = direct - - def cleanup(self) -> None: - if GH_KEEP_TMP: - print("upstream_dir preserved at: {}".format(self.upstream_sh.cwd)) - print("local_dir preserved at: {}".format(self.sh.cwd)) - else: - shutil.rmtree( - self.upstream_sh.cwd, - onerror=handle_remove_read_only, - ) - shutil.rmtree( - self.sh.cwd, - onerror=handle_remove_read_only, - ) - - def check_global_github_invariants(self, direct: bool) -> None: - r = self.github.graphql( - """ - query { - repository(name: "pytorch", owner: "pytorch") { - pullRequests { - nodes { - baseRefName - headRefName - closed - } - } - } - } - """ - ) - # No refs may be reused for multiple open PRs - seen_refs = set() - for pr in r["data"]["repository"]["pullRequests"]["nodes"]: - if pr["closed"]: - continue - # In direct mode, only head refs may not be reused; - # base refs can be reused in octopus situations - if not direct: - assert pr["baseRefName"] not in seen_refs - seen_refs.add(pr["baseRefName"]) - assert pr["headRefName"] not in seen_refs - seen_refs.add(pr["headRefName"]) - - -CTX: Context = None # type: ignore - - -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) - atexit.register(CTX.cleanup) - return CTX - - -@contextlib.contextmanager -def scoped_test(direct: bool) -> Iterator[None]: - global CTX - assert CTX is None - try: - CTX = Context(direct) - yield - finally: - CTX.cleanup() - CTX = None - - -# NB: returns earliest first -def gh_submit( - msg: str = "Update", - update_fields: bool = False, - short: bool = False, - no_skip: bool = False, - base: Optional[str] = None, - revs: Sequence[str] = (), - stack: bool = True, -) -> List[ghstack.submit.DiffMeta]: - self = CTX - r = ghstack.submit.main( - msg=msg, - username="ezyang", - github=self.github, - sh=self.sh, - update_fields=update_fields, - stack_header="Stack", - repo_owner_opt="pytorch", - repo_name_opt="pytorch", - short=short, - direct_opt=self.direct, - no_skip=no_skip, - github_url="github.com", - remote_name="origin", - base_opt=base, - revs=revs, - stack=stack, - check_invariants=True, - ) - self.check_global_github_invariants(self.direct) - return r - - -def gh_land(pull_request: str) -> None: - self = CTX - return ghstack.land.main( - remote_name="origin", - pull_request=pull_request, - github=self.github, - sh=self.sh, - github_url="github.com", - ) - - -def gh_unlink() -> None: - self = CTX - ghstack.unlink.main( - github=self.github, - sh=self.sh, - repo_owner="pytorch", - repo_name="pytorch", - github_url="github.com", - remote_name="origin", - ) - - -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) - - -def commit(name: str, msg: Optional[str] = None) -> None: - self = CTX - write_file_and_add(f"{name}.txt", "A") - self.sh.git( - "commit", - "-m", - f"Commit {name}\n\nThis is commit {name}" if msg is None else msg, - ) - self.sh.test_tick() - - -def amend(name: str) -> None: - self = CTX - write_file_and_add(f"{name}.txt", "A") - self.sh.git("commit", "--amend", "--no-edit", tick=True) - - -def git(*args: Any, **kwargs: Any) -> Any: - return CTX.sh.git(*args, **kwargs) - - -def ok() -> None: - print("\033[92m" + "TEST PASSED" + "\033[0m") - - -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) - - -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) - - -def dump_github() -> str: - self = CTX - r = self.github.graphql( - """ - query { - repository(name: "pytorch", owner: "pytorch") { - pullRequests { - nodes { - number - baseRefName - headRefName - title - body - closed - } - } - } - } - """ - ) - prs = [] - for pr in r["data"]["repository"]["pullRequests"]["nodes"]: - pr["body"] = indent(pr["body"].replace("\r", ""), " ") - # TODO: Use of git --graph here is a bit of a loaded - # footgun, because git doesn't really give any guarantees - # about what the graph should look like. So there isn't - # really any assurance that this will output the same thing - # on multiple test runs. We'll have to reimplement this - # ourselves to do it right. - # - # UPDATE: Another good reason to rewrite this is because git - # 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( - "log", - "--graph", - "--oneline", - "--pretty=format:%h %s", - f'{pr["baseRefName"]}..{pr["headRefName"]}', - ) - pr["commits"] = indent(strip_trailing_whitespace(pr["commits"]), " ") - else: - pr["commits"] = " (omitted)" - pr["status"] = "[X]" if pr["closed"] else "[O]" - prs.append( - "{status} #{number} {title} ({headRefName} -> {baseRefName})\n\n" - "{body}\n\n{commits}\n\n".format(**pr) - ) - - refs = self.upstream_sh.git( - "log", - "--graph", - "--oneline", - "--branches=gh/*/*/next", - "--branches=gh/*/*/head", - "--pretty=format:%h%d%n%w(0,3,3)%s", - ) - prs.append( - "Repository state:\n\n" + indent(strip_trailing_whitespace(refs), " ") + "\n" - ) - 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) - - -def is_direct() -> bool: - return CTX.direct - - -def assert_eq(a: Any, b: Any) -> None: - assert a == b, f"{a} != {b}" - - -def assert_raises( - exc_type: any, callable: Callable[..., any], *args: any, **kwargs: any -): - try: - callable(*args, **kwargs) - except exc_type: - return - assert False, "did not raise when expected to" - - -def assert_expected_raises_inline( - exc_type: any, callable: Callable[..., any], expect: str, *args: any, **kwargs: any -): - try: - callable(*args, **kwargs) - except exc_type as e: - assert_expected_inline(str(e), expect, skip=1) - return - assert False, "did not raise when expected to" - - -def get_sh() -> ghstack.shell.Shell: - return CTX.sh - - -def get_upstream_sh() -> ghstack.shell.Shell: - return CTX.upstream_sh - - -def get_github() -> ghstack.github.GitHubEndpoint: - return CTX.github - - -def tick() -> None: - CTX.sh.test_tick() diff --git a/src/ghstack/ghstack/trailers.py b/src/ghstack/ghstack/trailers.py deleted file mode 100644 index 0b788b7..0000000 --- a/src/ghstack/ghstack/trailers.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 - -import re -from typing import List, Tuple - -# Compile regexes once at module level for better performance -TRAILER_RE = re.compile(r"^([A-Za-z0-9_-]+)(\s*:\s*)(.*)$") -CONTINUATION_RE = re.compile(r"^\s+\S.*$") - -# Git-generated trailer prefixes -GIT_GENERATED_PREFIXES = ["Signed-off-by: ", "(cherry picked from commit "] - - -def parse_message(message: str) -> Tuple[str, str, str]: - """ - Parse a Git commit message into subject, body, and trailers. - - According to the Git documentation, trailers are: - - A group of one or more lines that is all trailers, or contains at least one - Git-generated or user-configured trailer and consists of at least 25% trailers. - - The group must be preceded by one or more empty (or whitespace-only) lines. - - The group must either be at the end of the message or be the last non-whitespace - lines before a line that starts with "---" (the "divider"). - - Args: - message: The commit message to parse. - - Returns: - A tuple containing: - - subject: The first line of the message - - body: The body of the message (may be empty) - - trailers: The trailer block as a raw string (may be empty) - """ - if not message: - return "", "", "" - - # Split into lines and get the subject (first line) - lines = message.splitlines() - subject = lines[0] if lines else "" - - if len(lines) <= 1: - return subject, "", "" - - # Remove subject - message_lines = lines[1:] - - if not message_lines: - return subject, "", "" - - # Find where the trailer block starts - trailer_start = find_trailer_block_start(message_lines) - - if trailer_start == -1: - # No trailer block found, everything after subject is body - body = "\n".join(message_lines).strip() - return subject, body, "" - - # Body is everything between subject and trailers (with empty lines trimmed) - body = "\n".join(message_lines[:trailer_start]).strip() - - # Keep trailers as a raw string - trailers = "\n".join(message_lines[trailer_start:]).strip() - - return subject, body, trailers - - -def find_trailer_block_start(lines: List[str]) -> int: - """ - Find the start index of the trailer block in a list of lines. - - Args: - lines: List of message lines (without subject and divider). - - Returns: - Index of the first line of the trailer block, or -1 if no trailer block is found. - """ - # Remove trailing empty lines - trimmed_lines = list(reversed([line for line in reversed(lines) if line.strip()])) - - if not trimmed_lines: - return -1 - - # Find the last non-empty block - block_indices = [-1] + [i for i, line in enumerate(lines) if not line.strip()] - - # Try blocks from last to first - for i in range(len(block_indices) - 1, -1, -1): - start_idx = block_indices[i] + 1 - # If we're at the beginning or checking the whole message - if i == 0 or start_idx == 0: - # Check if the whole remaining content is a trailer block - if is_trailer_block(lines[start_idx:]): - return start_idx - # No more blocks to check - return -1 - - # Check if the block after this blank line is a trailer block - end_idx = block_indices[i + 1] if i + 1 < len(block_indices) else len(lines) - if is_trailer_block(lines[start_idx:end_idx]): - return start_idx - - return -1 - - -def is_trailer_block(lines: List[str]) -> bool: - """ - Determine if the given lines form a trailer block. - - A block is a trailer block if: - 1. All lines are trailers, or - 2. At least one Git-generated trailer exists and at least 25% of lines are trailers - - Args: - lines: List of lines to check. - - Returns: - True if the lines form a trailer block, False otherwise. - """ - # Filter out empty lines - content_lines = [line for line in lines if line.strip()] - - if not content_lines: - return False - - trailer_lines = 0 - non_trailer_lines = 0 - has_git_generated_trailer = False - - i = 0 - while i < len(content_lines): - line = content_lines[i] - - # Skip continuation lines (they belong to the previous trailer) - if CONTINUATION_RE.match(line): - i += 1 - continue - - # Check if it's a git-generated trailer - if any(line.startswith(prefix) for prefix in GIT_GENERATED_PREFIXES): - has_git_generated_trailer = True - trailer_lines += 1 - elif TRAILER_RE.match(line): - # Regular trailer - trailer_lines += 1 - else: - # Not a trailer line - non_trailer_lines += 1 - - i += 1 - - # A block is a trailer block if all lines are trailers OR - # it has at least one git-generated trailer and >= 25% of lines are trailers - return (trailer_lines > 0 and non_trailer_lines == 0) or ( - has_git_generated_trailer and trailer_lines * 3 >= non_trailer_lines - ) - - -def interpret_trailers(message: str, trailers_to_add: List[str]) -> str: - """ - Add trailers to a commit message, mimicking git interpret-trailers. - - Args: - message: The commit message to add trailers to - trailers_to_add: List of trailers to add in the format "Key: Value" - - Returns: - The commit message with trailers added - """ - subject, body, existing_trailers = parse_message(message) - - # Create a new list with all trailers (existing + new) - all_trailers = [] - if existing_trailers: - all_trailers.append(existing_trailers) - - all_trailers.extend(trailers_to_add) - - # Build the new message - new_message = subject - - if body: - new_message += "\n\n" + body - - if all_trailers: - if body or (not body and existing_trailers): - new_message += "\n" - if not existing_trailers: - new_message += "\n" - new_message += "\n" + "\n".join(all_trailers) - - return new_message diff --git a/src/ghstack/ghstack/types.py b/src/ghstack/ghstack/types.py deleted file mode 100644 index 640a253..0000000 --- a/src/ghstack/ghstack/types.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -from typing import NewType - -# A bunch of commonly used type definitions. - -PhabricatorDiffNumberWithD = NewType( - "PhabricatorDiffNumberWithD", str -) # aka "D1234567" - -GitHubNumber = NewType("GitHubNumber", int) # aka 1234 (as in #1234) - -# GraphQL ID that identifies Repository from GitHubb schema; -# aka MDExOlB1bGxSZXF1ZXN0MjU2NDM3MjQw -GitHubRepositoryId = NewType("GitHubRepositoryId", str) - -# aka 12 (as in gh/ezyang/12/base) -GhNumber = NewType("GhNumber", str) - -# Actually, sometimes we smuggle revs in here. We shouldn't. -# We want to guarantee that they're full canonical revs so that -# you can do equality on them without fear. -# commit 3f72e04eeabcc7e77f127d3e7baf2f5ccdb148ee -GitCommitHash = NewType("GitCommitHash", str) - -# tree 3f72e04eeabcc7e77f127d3e7baf2f5ccdb148ee -GitTreeHash = NewType("GitTreeHash", str) diff --git a/src/ghstack/ghstack/unlink.py b/src/ghstack/ghstack/unlink.py deleted file mode 100644 index ce790f3..0000000 --- a/src/ghstack/ghstack/unlink.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 - -import logging -import re -import textwrap -from typing import List, Optional, Set - -import ghstack.diff -import ghstack.git -import ghstack.github -import ghstack.github_utils -import ghstack.gpg_sign -import ghstack.shell -from ghstack.types import GitCommitHash - -RE_GHSTACK_SOURCE_ID = re.compile(r"^ghstack-source-id: (.+)\n?", re.MULTILINE) - - -def main( - *, - commits: Optional[List[str]] = None, - github: ghstack.github.GitHubEndpoint, - sh: Optional[ghstack.shell.Shell] = None, - repo_owner: Optional[str] = None, - repo_name: Optional[str] = None, - github_url: str, - remote_name: str, - repo_default_branch_opt: Optional[str] = None, -) -> GitCommitHash: - # If commits is empty, we unlink the entire stack - # - # For now, we only process commits on our current - # stack, because we have no way of knowing how to - # "restack" for other commits. - - if sh is None: - # Use CWD - sh = ghstack.shell.Shell() - - default_branch = repo_default_branch_opt - if default_branch is None: - default_branch = 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"] - - # 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))) - - base = GitCommitHash( - sh.git("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") - ) - - # sanity check the parsed_commits - if parsed_commits is not None: - stack_commits = set() - for s in stack: - stack_commits.add(s.commit_id) - invalid_commits = parsed_commits - stack_commits - if invalid_commits: - raise RuntimeError( - "unlink can only process commits which are on the " - "current stack; these commits are not:\n{}".format( - "\n".join(invalid_commits) - ) - ) - - # Run the interactive rebase. Don't start rewriting until we - # hit the first commit that needs it. - head = base - rewriting = False - - for s in stack: - commit_id = s.commit_id - should_unlink = parsed_commits is None or commit_id in parsed_commits - if not rewriting and not should_unlink: - # Advance HEAD without reconstructing commit - head = commit_id - continue - - rewriting = True - commit_msg = s.commit_msg - logging.debug("-- commit_msg:\n{}".format(textwrap.indent(commit_msg, " "))) - if should_unlink: - commit_msg = RE_GHSTACK_SOURCE_ID.sub( - "", - ghstack.diff.re_pull_request_resolved_w_sp(github_url).sub( - "", commit_msg - ), - ) - logging.debug( - "-- edited commit_msg:\n{}".format(textwrap.indent(commit_msg, " ")) - ) - head = GitCommitHash( - sh.git( - "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(sh), - s.tree, - "-p", - head, - input=commit_msg, - ) - ) - - sh.git("reset", "--soft", head) - - logging.info( - """ -Diffs successfully unlinked! - -To undo this operation, run: - - git reset --soft {} -""".format( - s.commit_id - ) - ) - - return head diff --git a/src/ghstack/land.py b/src/ghstack/land.py index 4a96d60..5e411fe 100644 --- a/src/ghstack/land.py +++ b/src/ghstack/land.py @@ -2,7 +2,7 @@ import logging import re -from typing import List, Tuple +from typing import List, Optional, Tuple import ghstack.git import ghstack.github @@ -50,6 +50,7 @@ def main( github_url: str, *, force: bool = False, + repo_default_branch_opt: Optional[str] = None, ) -> None: # We land the entire stack pointed to by a URL. @@ -60,14 +61,16 @@ def main( params = ghstack.github_utils.parse_pull_request( pull_request, sh=sh, remote_name=remote_name ) - default_branch = 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_default_branch_opt + if default_branch is None: + default_branch = 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"] needs_force = False try: diff --git a/src/ghstack/submit.py b/src/ghstack/submit.py index f7910c5..797d00f 100644 --- a/src/ghstack/submit.py +++ b/src/ghstack/submit.py @@ -308,6 +308,11 @@ class Submitter: # Presents as repo_name kwarg in main repo_name_opt: Optional[str] = None + # allow configuration to optionally define repo details + repo_is_fork_opt: Optional[str] = None + repo_id_opt: Optional[str] = None + repo_default_branch_opt: Optional[str] = None + # Print only PR URL to stdout short: bool = False @@ -375,17 +380,32 @@ class Submitter: # Post initialization def __post_init__(self) -> None: - # Network call in the constructor, help me father, for I have sinned - repo = ghstack.github_utils.get_github_repo_info( - github=self.github, - sh=self.sh, - repo_owner=self.repo_owner_opt, - repo_name=self.repo_name_opt, - github_url=self.github_url, - remote_name=self.remote_name, - ) - object.__setattr__(self, "repo_owner", repo["name_with_owner"]["owner"]) - object.__setattr__(self, "repo_name", repo["name_with_owner"]["name"]) + repo = {} + if ( + self.repo_id_opt is not None + and self.repo_owner_opt is not None + and self.repo_name_opt is not None + and self.repo_is_fork_opt is not None + and self.repo_default_branch_opt is not None + ): + repo["is_fork"] = self.repo_is_fork_opt + repo["id"] = self.repo_id_opt + repo["default_branch"] = self.repo_default_branch_opt + repo["name_with_owner"] = {} + repo["name_with_owner"]["owner"] = self.repo_owner_opt + repo["name_with_owner"]["name"] = self.repo_name_opt + else: + # Network call in the constructor, help me father, for I have sinned + repo = ghstack.github_utils.get_github_repo_info( + github=self.github, + sh=self.sh, + repo_owner=self.repo_owner_opt, + repo_name=self.repo_name_opt, + github_url=self.github_url, + remote_name=self.remote_name, + ) + object.__setattr__(self, "repo_owner", repo["name_with_owner"]["owner"]) + object.__setattr__(self, "repo_name", repo["name_with_owner"]["name"]) if repo["is_fork"]: raise RuntimeError( diff --git a/src/ghstack/unlink.py b/src/ghstack/unlink.py index 611f2e6..ce790f3 100644 --- a/src/ghstack/unlink.py +++ b/src/ghstack/unlink.py @@ -25,6 +25,7 @@ def main( repo_name: Optional[str] = None, github_url: str, remote_name: str, + repo_default_branch_opt: Optional[str] = None, ) -> GitCommitHash: # If commits is empty, we unlink the entire stack # @@ -36,14 +37,16 @@ def main( # Use CWD sh = ghstack.shell.Shell() - default_branch = 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_default_branch_opt + if default_branch is None: + default_branch = 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"] # Parse the commits parsed_commits: Optional[Set[GitCommitHash]] = None