pax_global_header00006660000000000000000000000064145713774150014530gustar00rootroot0000000000000052 comment=3ca179bcdeb46b5e54ddc6cad8feb6addf487d7c cli-2.45.0/000077500000000000000000000000001457137741500123675ustar00rootroot00000000000000cli-2.45.0/.devcontainer/000077500000000000000000000000001457137741500151265ustar00rootroot00000000000000cli-2.45.0/.devcontainer/devcontainer.json000066400000000000000000000006771457137741500205140ustar00rootroot00000000000000{ "image": "mcr.microsoft.com/devcontainers/go:1.21", "features": { "ghcr.io/devcontainers/features/sshd:1": {} }, "remoteUser": "vscode", "customizations": { "vscode": { "extensions": [ "golang.go" ], "settings": { "go.toolsManagement.checkForUpdates": "local", "go.useLanguageServer": true, "go.gopath": "/go" } } }, "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ] } cli-2.45.0/.gitattributes000066400000000000000000000000531457137741500152600ustar00rootroot00000000000000.github/actions/*/lib/* linguist-generated cli-2.45.0/.github/000077500000000000000000000000001457137741500137275ustar00rootroot00000000000000cli-2.45.0/.github/CODE-OF-CONDUCT.md000066400000000000000000000064521457137741500163710ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@github.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq cli-2.45.0/.github/CODEOWNERS000066400000000000000000000001371457137741500153230ustar00rootroot00000000000000* @cli/code-reviewers pkg/cmd/codespace/ @cli/codespaces internal/codespaces/ @cli/codespaces cli-2.45.0/.github/CONTRIBUTING.md000066400000000000000000000065671457137741500161760ustar00rootroot00000000000000## Contributing Hi! Thanks for your interest in contributing to the GitHub CLI! We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues. Please do: * Check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted. * Open an issue if things aren't working as expected. * Open an issue to propose a significant change. * Open a pull request to fix a bug. * Open a pull request to fix documentation about a command. * Open a pull request for any issue labelled [`help wanted`][hw] or [`good first issue`][gfi]. Please avoid: * Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`. * Opening pull requests that haven't been approved for work in an issue * Adding installation instructions specifically for your OS/package manager. * Opening pull requests for any issue marked `core`. These issues require additional context from the core CLI team at GitHub and any external pull requests will not be accepted. ## Building the project Prerequisites: - Go 1.21+ Build with: * Unix-like systems: `make` * Windows: `go run script/build.go` Run the new binary as: * Unix-like systems: `bin/gh` * Windows: `bin\gh` Run tests with: `go test ./...` See [project layout documentation](../docs/project-layout.md) for information on where to find specific source files. ## Submitting a pull request 1. Create a new branch: `git checkout -b my-branch-name` 1. Make your change, add tests, and ensure tests pass 1. Submit a pull request: `gh pr create --web` Contributions to this project are [released][legal] to the public under the [project's open source license][license]. Please note that this project adheres to a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. We generate manual pages from source on every release. You do not need to submit pull requests for documentation specifically; manual pages for commands will automatically get updated after your pull requests gets accepted. ## Design guidelines You may reference the [CLI Design System][] when suggesting features, and are welcome to use our [Google Docs Template][] to suggest designs. ## Resources - [How to Contribute to Open Source][] - [Using Pull Requests][] - [GitHub Help][] [bug issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Abug [feature request issues]: https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement [hw]: https://github.com/cli/cli/labels/help%20wanted [gfi]: https://github.com/cli/cli/labels/good%20first%20issue [legal]: https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-terms-of-service#6-contributions-under-repository-license [license]: ../LICENSE [code-of-conduct]: ./CODE-OF-CONDUCT.md [How to Contribute to Open Source]: https://opensource.guide/how-to-contribute/ [Using Pull Requests]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests [GitHub Help]: https://docs.github.com/ [CLI Design System]: https://primer.style/cli/ [Google Docs Template]: https://docs.google.com/document/d/1JIRErIUuJ6fTgabiFYfCH3x91pyHuytbfa0QLnTfXKM/edit#heading=h.or54sa47ylpg cli-2.45.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457137741500161125ustar00rootroot00000000000000cli-2.45.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010471457137741500206060ustar00rootroot00000000000000--- name: "\U0001F41B Bug report" about: Report a bug or unexpected behavior while using GitHub CLI title: '' labels: bug assignees: '' --- ### Describe the bug A clear and concise description of what the bug is. Include version by typing `gh --version`. ### Steps to reproduce the behavior 1. Type this '...' 2. View the output '....' 3. See error ### Expected vs actual behavior A clear and concise description of what you expected to happen and what actually happened. ### Logs Paste the activity from your command line. Redact if needed. cli-2.45.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000006421457137741500201040ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Ask a question on how to use GitHub CLI about: For general-purpose questions and answers, see the Discussions section. url: https://github.com/cli/cli/discussions - name: Ask a question about the GitHub API about: Please check out the GitHub community forum for discussions about the GitHub API. url: https://github.community/c/github-ecosystem/37 cli-2.45.0/.github/ISSUE_TEMPLATE/feedback.md000066400000000000000000000007671457137741500201720ustar00rootroot00000000000000--- name: "\U0001F4E3 Feedback" about: Give us general feedback about the GitHub CLI title: '' labels: feedback assignees: '' --- # CLI Feedback You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you! ## What have you loved? _eg "the nice colors"_ ## What was confusing or gave you pause? _eg "it did something unexpected"_ ## Are there features you'd like to see added? _eg "gh cli needs mini-games"_ ## Anything else? _eg "have a nice day"_ cli-2.45.0/.github/ISSUE_TEMPLATE/submit-a-request.md000066400000000000000000000006731457137741500216510ustar00rootroot00000000000000--- name: "⭐ Submit a request" about: Surface a feature or problem that you think should be solved title: '' labels: enhancement assignees: '' --- ### Describe the feature or problem you’d like to solve A clear and concise description of what the feature or problem is. ### Proposed solution How will it benefit CLI and its users? ### Additional context Add any other context like screenshots or mockups are helpful, if applicable. cli-2.45.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002121457137741500175230ustar00rootroot00000000000000 cli-2.45.0/.github/SECURITY.md000066400000000000000000000014651457137741500155260ustar00rootroot00000000000000GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [cli](https://github.com/cli). If you believe you have found a security vulnerability in GitHub CLI, you can report it to us in one of two ways: * Report it to this repository directly using [private vulnerability reporting][]. Such reports are not eligible for a bounty reward. * Submit the report through [HackerOne][] to be eligible for a bounty reward. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Thanks for helping make GitHub safe for everyone. [private vulnerability reporting]: https://github.com/cli/cli/security/advisories [HackerOne]: https://hackerone.com/github cli-2.45.0/.github/dependabot.yml000066400000000000000000000005021457137741500165540ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: "daily" ignore: - dependency-name: "*" update-types: - version-update:semver-minor - version-update:semver-major - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" cli-2.45.0/.github/workflows/000077500000000000000000000000001457137741500157645ustar00rootroot00000000000000cli-2.45.0/.github/workflows/codeql.yml000066400000000000000000000013641457137741500177620ustar00rootroot00000000000000name: Code Scanning on: push: branches: [trunk] pull_request: branches: [trunk] paths-ignore: - '**/*.md' schedule: - cron: "0 0 * * 0" permissions: actions: read # for github/codeql-action/init to get workflow details contents: read # for actions/checkout to fetch code security-events: write # for github/codeql-action/analyze to upload SARIF results jobs: CodeQL-Build: runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: go queries: security-and-quality - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 cli-2.45.0/.github/workflows/deployment.yml000066400000000000000000000337161457137741500207010ustar00rootroot00000000000000name: Deployment run-name: ${{ inputs.tag_name }} / go ${{ inputs.go_version }} / ${{ inputs.environment }} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: true permissions: contents: write on: workflow_dispatch: inputs: tag_name: required: true type: string environment: default: production type: environment go_version: default: "1.21" type: string platforms: default: "linux,macos,windows" type: string release: description: "Whether to create a GitHub Release" type: boolean default: true jobs: linux: runs-on: ubuntu-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'linux') steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go_version }} - name: Install GoReleaser uses: goreleaser/goreleaser-action@v5 with: version: "~1.17.1" install-only: true - name: Build release binaries env: TAG_NAME: ${{ inputs.tag_name }} run: script/release --local "$TAG_NAME" --platform linux - name: Generate web manual pages run: | go run ./cmd/gen-docs --website --doc-path dist/manual tar -czvf dist/manual.tar.gz -C dist -- manual - uses: actions/upload-artifact@v4 with: name: linux if-no-files-found: error retention-days: 7 path: | dist/*.tar.gz dist/*.rpm dist/*.deb macos: runs-on: macos-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'macos') steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go_version }} - name: Configure macOS signing if: inputs.environment == 'production' env: APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} APPLE_APPLICATION_CERT: ${{ secrets.APPLE_APPLICATION_CERT }} APPLE_APPLICATION_CERT_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERT_PASSWORD }} run: | keychain="$RUNNER_TEMP/buildagent.keychain" keychain_password="password1" security create-keychain -p "$keychain_password" "$keychain" security default-keychain -s "$keychain" security unlock-keychain -p "$keychain_password" "$keychain" base64 -D <<<"$APPLE_APPLICATION_CERT" > "$RUNNER_TEMP/cert.p12" security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_APPLICATION_CERT_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "$keychain_password" "$keychain" rm "$RUNNER_TEMP/cert.p12" - name: Install GoReleaser uses: goreleaser/goreleaser-action@v5 with: version: "~1.17.1" install-only: true - name: Build release binaries env: TAG_NAME: ${{ inputs.tag_name }} APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} run: script/release --local "$TAG_NAME" --platform macos - name: Notarize macOS archives if: inputs.environment == 'production' env: APPLE_ID: ${{ vars.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_DEVELOPER_ID: ${{ vars.APPLE_DEVELOPER_ID }} run: | shopt -s failglob script/sign dist/gh_*_macOS_*.zip - uses: actions/upload-artifact@v4 with: name: macos if-no-files-found: error retention-days: 7 path: | dist/*.tar.gz dist/*.zip windows: runs-on: windows-latest environment: ${{ inputs.environment }} if: contains(inputs.platforms, 'windows') steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go_version }} - name: Install GoReleaser uses: goreleaser/goreleaser-action@v5 with: version: "~1.17.1" install-only: true - name: Install Azure Code Signing Client shell: pwsh env: ACS_DIR: ${{ runner.temp }}\acs ACS_ZIP: ${{ runner.temp }}\acs.zip CORRELATION_ID: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} METADATA_PATH: ${{ runner.temp }}\acs\metadata.json run: | # Download Azure Code Signing client containing the DLL needed for signtool in script/sign Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Azure.CodeSigning.Client/1.0.43 -OutFile $Env:ACS_ZIP -Verbose Expand-Archive $Env:ACS_ZIP -Destination $Env:ACS_DIR -Force -Verbose # Generate metadata file for signtool, used in signing box .exe and .msi @{ CertificateProfileName = "GitHubInc" CodeSigningAccountName = "GitHubInc" CorrelationId = $Env:CORRELATION_ID Endpoint = "https://wus.codesigning.azure.net/" } | ConvertTo-Json | Out-File -FilePath $Env:METADATA_PATH # Azure Code Signing leverages the environment variables for secrets that complement the metadata.json # file generated above (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID) # For more information, see https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet - name: Build release binaries shell: bash env: AZURE_CLIENT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.SPN_GITHUB_CLI_SIGNING }} AZURE_TENANT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_TENANT_ID }} DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json TAG_NAME: ${{ inputs.tag_name }} run: script/release --local "$TAG_NAME" --platform windows - name: Set up MSBuild id: setupmsbuild uses: microsoft/setup-msbuild@v2.0.0 - name: Build MSI shell: bash env: MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }} run: | for ZIP_FILE in dist/gh_*_windows_*.zip; do MSI_NAME="$(basename "$ZIP_FILE" ".zip")" MSI_VERSION="$(cut -d_ -f2 <<<"$MSI_NAME" | cut -d- -f1)" case "$MSI_NAME" in *_386 ) source_dir="$PWD/dist/windows_windows_386" platform="x86" ;; *_amd64 ) source_dir="$PWD/dist/windows_windows_amd64_v1" platform="x64" ;; *_arm64 ) echo "skipping building MSI for arm64 because WiX 3.11 doesn't support it: https://github.com/wixtoolset/issues/issues/6141" >&2 continue #source_dir="$PWD/dist/windows_windows_arm64" #platform="arm64" ;; * ) printf "unsupported architecture: %s\n" "$MSI_NAME" >&2 exit 1 ;; esac "${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$source_dir" -p:OutputPath="$PWD/dist" -p:OutputName="$MSI_NAME" -p:ProductVersion="${MSI_VERSION#v}" -p:Platform="$platform" done - name: Sign .msi release binaries if: inputs.environment == 'production' shell: pwsh env: AZURE_CLIENT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.SPN_GITHUB_CLI_SIGNING }} AZURE_TENANT_ID: ${{ secrets.SPN_GITHUB_CLI_SIGNING_TENANT_ID }} DLIB_PATH: ${{ runner.temp }}\acs\bin\x64\Azure.CodeSigning.Dlib.dll METADATA_PATH: ${{ runner.temp }}\acs\metadata.json run: | Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object { .\script\sign.ps1 $_.FullName } - uses: actions/upload-artifact@v4 with: name: windows if-no-files-found: error retention-days: 7 path: | dist/*.zip dist/*.msi release: runs-on: ubuntu-latest needs: [linux, macos, windows] environment: ${{ inputs.environment }} if: inputs.release steps: - name: Checkout cli/cli uses: actions/checkout@v4 - name: Merge built artifacts uses: actions/download-artifact@v4 - name: Checkout documentation site uses: actions/checkout@v4 with: repository: github/cli.github.com path: site fetch-depth: 0 token: ${{ secrets.SITE_DEPLOY_PAT }} - name: Update site man pages env: GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation GIT_COMMITTER_EMAIL: noreply@github.com GIT_AUTHOR_EMAIL: noreply@github.com TAG_NAME: ${{ inputs.tag_name }} run: | git -C site rm 'manual/gh*.md' 2>/dev/null || true tar -xzvf linux/manual.tar.gz -C site git -C site add 'manual/gh*.md' sed -i.bak -E "s/(assign version = )\".+\"/\1\"${TAG_NAME#v}\"/" site/index.html rm -f site/index.html.bak git -C site add index.html git -C site diff --quiet --cached || git -C site commit -m "gh ${TAG_NAME#v}" - name: Prepare release assets env: TAG_NAME: ${{ inputs.tag_name }} run: | shopt -s failglob rm -rf dist mkdir dist mv -v {linux,macos,windows}/gh_* dist/ - name: Install packaging dependencies run: sudo apt-get install -y rpm reprepro - name: Set up GPG if: inputs.environment == 'production' env: GPG_PUBKEY: ${{ secrets.GPG_PUBKEY }} GPG_KEY: ${{ secrets.GPG_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_KEYGRIP: ${{ secrets.GPG_KEYGRIP }} run: | base64 -d <<<"$GPG_PUBKEY" | gpg --import --no-tty --batch --yes base64 -d <<<"$GPG_KEY" | gpg --import --no-tty --batch --yes echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf gpg-connect-agent RELOADAGENT /bye /usr/lib/gnupg2/gpg-preset-passphrase --preset "$GPG_KEYGRIP" <<<"$GPG_PASSPHRASE" - name: Sign RPMs if: inputs.environment == 'production' run: | cp script/rpmmacros ~/.rpmmacros rpmsign --addsign dist/*.rpm - name: Run createrepo env: GPG_SIGN: ${{ inputs.environment == 'production' }} run: | mkdir -p site/packages/rpm cp dist/*.rpm site/packages/rpm/ ./script/createrepo.sh cp -r dist/repodata site/packages/rpm/ pushd site/packages/rpm [ "$GPG_SIGN" = "false" ] || gpg --yes --detach-sign --armor repodata/repomd.xml popd - name: Run reprepro env: GPG_SIGN: ${{ inputs.environment == 'production' }} # We are no longer adding to the distribution list. # All apt distributions should use "stable" according to our install documentation. # In the future we will remove legacy distributions listed here. RELEASES: "cosmic eoan disco groovy focal stable oldstable testing sid unstable buster bullseye stretch jessie bionic trusty precise xenial hirsute impish kali-rolling" run: | mkdir -p upload [ "$GPG_SIGN" = "true" ] || sed -i.bak '/^SignWith:/d' script/distributions for release in $RELEASES; do for file in dist/*.deb; do reprepro --confdir="+b/script" includedeb "$release" "$file" done done cp -a dists/ pool/ upload/ mkdir -p site/packages cp -a upload/* site/packages/ - name: Create the release env: # In non-production environments, the assets will not have been signed DO_PUBLISH: ${{ inputs.environment == 'production' }} TAG_NAME: ${{ inputs.tag_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | shopt -s failglob pushd dist shasum -a 256 gh_* > checksums.txt mv checksums.txt gh_${TAG_NAME#v}_checksums.txt popd release_args=( "$TAG_NAME" --title "GitHub CLI ${TAG_NAME#v}" --target "$GITHUB_SHA" --generate-notes ) if [[ $TAG_NAME == *-* ]]; then release_args+=( --prerelease ) else release_args+=( --discussion-category "General" ) fi guard="echo" [ "$DO_PUBLISH" = "false" ] || guard="" script/label-assets dist/gh_* | xargs $guard gh release create "${release_args[@]}" -- - name: Publish site env: DO_PUBLISH: ${{ inputs.environment == 'production' && !contains(inputs.tag_name, '-') }} TAG_NAME: ${{ inputs.tag_name }} GIT_COMMITTER_NAME: cli automation GIT_AUTHOR_NAME: cli automation GIT_COMMITTER_EMAIL: noreply@github.com GIT_AUTHOR_EMAIL: noreply@github.com working-directory: ./site run: | git add packages git commit -m "Add rpm and deb packages for $TAG_NAME" if [ "$DO_PUBLISH" = "true" ]; then git push else git log --oneline @{upstream}.. git diff --name-status @{upstream}.. fi - name: Bump homebrew-core formula uses: mislav/bump-homebrew-formula-action@v3 if: inputs.environment == 'production' && !contains(inputs.tag_name, '-') with: formula-name: gh formula-path: Formula/g/gh.rb tag-name: ${{ inputs.tag_name }} push-to: williammartin/homebrew-core env: COMMITTER_TOKEN: ${{ secrets.HOMEBREW_PR_PAT }} cli-2.45.0/.github/workflows/go.yml000066400000000000000000000014611457137741500171160ustar00rootroot00000000000000name: Tests on: [push, pull_request] permissions: contents: read jobs: build: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - name: Set up Go 1.21 uses: actions/setup-go@v5 with: go-version: 1.21 - name: Check out code uses: actions/checkout@v4 - name: Restore Go modules cache uses: actions/cache@v4 with: path: ~/go/pkg/mod key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} restore-keys: | go-${{ runner.os }}- - name: Download dependencies run: go mod download - name: Run tests run: go test -race ./... - name: Build run: go build -v ./cmd/gh cli-2.45.0/.github/workflows/homebrew-bump.yml000066400000000000000000000011441457137741500212600ustar00rootroot00000000000000name: homebrew-bump-debug permissions: contents: write on: workflow_dispatch: inputs: tag_name: required: true type: string environment: default: production type: environment jobs: bump: runs-on: ubuntu-latest steps: - name: Bump homebrew-core formula uses: mislav/bump-homebrew-formula-action@v3 if: inputs.environment == 'production' && !contains(inputs.tag_name, '-') with: formula-name: gh tag-name: ${{ inputs.tag_name }} env: COMMITTER_TOKEN: ${{ secrets.HOMEBREW_PR_PAT }} cli-2.45.0/.github/workflows/issueauto.yml000066400000000000000000000011351457137741500205300ustar00rootroot00000000000000name: Issue Automation on: issues: types: [opened] permissions: contents: none issues: write jobs: issue-auto: runs-on: ubuntu-latest steps: - name: label incoming issue env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} ISSUENUM: ${{ github.event.issue.number }} ISSUEAUTHOR: ${{ github.event.issue.user.login }} run: | if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null then gh issue edit $ISSUENUM --add-label "needs-triage" ficli-2.45.0/.github/workflows/lint.yml000066400000000000000000000032541457137741500174610ustar00rootroot00000000000000name: Lint on: push: paths: - "**.go" - go.mod - go.sum pull_request: paths: - "**.go" - go.mod - go.sum permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - name: Set up Go 1.21 uses: actions/setup-go@v5 with: go-version: 1.21 - name: Check out code uses: actions/checkout@v4 - name: Restore Go modules cache uses: actions/cache@v4 with: path: ~/go/pkg/mod key: go-${{ runner.os }}-${{ hashFiles('go.mod') }} restore-keys: | go-${{ runner.os }}- - name: Verify dependencies run: | go mod verify go mod download LINT_VERSION=1.54.1 curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \ tar xz --strip-components 1 --wildcards \*/golangci-lint mkdir -p bin && mv golangci-lint bin/ - name: Run checks run: | STATUS=0 assert-nothing-changed() { local diff "$@" >/dev/null || return 1 if ! diff="$(git diff -U1 --color --exit-code)"; then printf '\e[31mError: running `\e[1m%s\e[22m` results in modifications that you must check into version control:\e[0m\n%s\n\n' "$*" "$diff" >&2 git checkout -- . STATUS=1 fi } assert-nothing-changed go fmt ./... assert-nothing-changed go mod tidy bin/golangci-lint run --out-format=github-actions --timeout=3m || STATUS=$? exit $STATUS cli-2.45.0/.github/workflows/prauto.yml000066400000000000000000000064461457137741500200330ustar00rootroot00000000000000name: PR Automation on: pull_request_target: types: [ready_for_review, opened, reopened] permissions: contents: none issues: write pull-requests: write jobs: pr-auto: runs-on: ubuntu-latest steps: - name: lint pr env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} PRID: ${{ github.event.pull_request.node_id }} PRBODY: ${{ github.event.pull_request.body }} PRNUM: ${{ github.event.pull_request.number }} PRHEAD: ${{ github.event.pull_request.head.label }} PRAUTHOR: ${{ github.event.pull_request.user.login }} PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }} if: "!github.event.pull_request.draft" run: | commentPR () { gh pr comment $PRNUM -b "${1}" } closePR () { gh pr close $PRNUM } colID () { gh api graphql -f query='query($owner:String!, $repo:String!) { repository(owner:$owner, name:$repo) { project(number:1) { columns(first:10) { nodes {id,name} } } } }' -f owner="${GH_REPO%/*}" -f repo="${GH_REPO#*/}" \ -q ".data.repository.project.columns.nodes[] | select(.name | startswith(\"$1\")) | .id" } addToBoard () { gh api graphql --silent -f query=' mutation($colID:ID!, $prID:ID!) { addProjectCard(input: { projectColumnId: $colID, contentId: $prID }) { clientMutationId } } ' -f colID="$(colID "Needs review")" -f prID="$PRID" } if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/$PRAUTHOR --silent 2>/dev/null then if [ "$PR_AUTHOR_TYPE" != "Bot" ] then gh pr edit $PRNUM --add-assignee $PRAUTHOR fi if ! errtext="$(addToBoard 2>&1)" then cat <<<"$errtext" >&2 if ! grep -iq 'project already has the associated issue' <<<"$errtext" then exit 1 fi fi exit 0 fi gh pr edit $PRNUM --add-label "external" if [ "$PRHEAD" = "cli:trunk" ] then closePR exit 0 fi if [ $(wc -c <<<"$PRBODY") -lt 10 ] then commentPR "Thanks for the pull request! We're a small team and it's helpful to have context around community submissions in order to review them appropriately. Our automation has closed this pull request since it does not have an adequate description. Please edit the body of this pull request to describe what this does, then reopen it." closePR exit 0 fi if ! grep -Eq '(#|issues/)[0-9]+' <<<"$PRBODY" then commentPR "Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message." fi addToBoard exit 0 cli-2.45.0/.github/workflows/triage.yml000066400000000000000000000047201457137741500177650ustar00rootroot00000000000000name: Discussion Triage run-name: ${{ github.event_name == 'issues' && github.event.issue.title || github.event.pull_request.title }} on: issues: types: - labeled pull_request_target: types: - labeled env: TARGET_REPO: github/cli jobs: issue: runs-on: ubuntu-latest if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'discuss' steps: - name: Create issue based on source issue env: BODY: ${{ github.event.issue.body }} CREATED: ${{ github.event.issue.created_at }} GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} LINK: ${{ github.repository }}#${{ github.event.issue.number }} TITLE: ${{ github.event.issue.title }} TRIGGERED_BY: ${{ github.triggering_actor }} run: | # Markdown quote source body by replacing newlines for newlines and markdown quoting BODY="${BODY//$'\n'/$'\n'> }" # Create issue using dynamically constructed body within heredoc cat << EOF | gh issue create --title "Triage issue \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage **Title:** $TITLE **Issue:** $LINK **Created:** $CREATED **Triggered by:** @$TRIGGERED_BY --- > $BODY EOF pull_request: runs-on: ubuntu-latest if: github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'discuss' steps: - name: Create issue based on source pull request env: BODY: ${{ github.event.pull_request.body }} CREATED: ${{ github.event.pull_request.created_at }} GH_TOKEN: ${{ secrets.CLI_DISCUSSION_TRIAGE_TOKEN }} LINK: ${{ github.repository }}#${{ github.event.pull_request.number }} TITLE: ${{ github.event.pull_request.title }} TRIGGERED_BY: ${{ github.triggering_actor }} run: | # Markdown quote source body by replacing newlines for newlines and markdown quoting BODY="${BODY//$'\n'/$'\n'> }" # Create issue using dynamically constructed body within heredoc cat << EOF | gh issue create --title "Triage PR \"$TITLE\"" --body-file - --repo "$TARGET_REPO" --label triage **Title:** $TITLE **Pull request:** $LINK **Created:** $CREATED **Triggered by:** @$TRIGGERED_BY --- > $BODY EOF cli-2.45.0/.gitignore000066400000000000000000000004751457137741500143650ustar00rootroot00000000000000/bin /share/bash-completion/completions /share/fish/vendor_completions.d /share/man/man1 /share/zsh/site-functions /gh-cli .envrc /dist /site .github/**/node_modules /CHANGELOG.md /.goreleaser.generated.yml /script/build /script/build.exe # VS Code .vscode # IntelliJ .idea # macOS .DS_Store # vim *.swp vendor/ cli-2.45.0/.golangci.yml000066400000000000000000000001451457137741500147530ustar00rootroot00000000000000linters: enable: - gofmt - nolintlint issues: max-issues-per-linter: 0 max-same-issues: 0 cli-2.45.0/.goreleaser.yml000066400000000000000000000054711457137741500153270ustar00rootroot00000000000000project_name: gh release: prerelease: auto draft: true # we only publish after the Windows MSI gets uploaded name_template: "GitHub CLI {{.Version}}" before: hooks: - >- {{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}} - >- {{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions builds: - id: macos #build:macos goos: [darwin] goarch: [amd64, arm64] hooks: post: - cmd: ./script/sign '{{ .Path }}' output: true binary: bin/gh main: ./cmd/gh ldflags: - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - id: linux #build:linux goos: [linux] goarch: [386, arm, amd64, arm64] env: - CGO_ENABLED=0 binary: bin/gh main: ./cmd/gh ldflags: - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} - id: windows #build:windows goos: [windows] goarch: [386, amd64, arm64] hooks: post: - cmd: >- {{ if eq .Runtime.Goos "windows" }}pwsh .\script\sign.ps1{{ else }}./script/sign{{ end }} '{{ .Path }}' output: true binary: bin/gh main: ./cmd/gh ldflags: - -s -w -X github.com/cli/cli/v2/internal/build.Version={{.Version}} -X github.com/cli/cli/v2/internal/build.Date={{time "2006-01-02"}} archives: - id: linux-archive builds: [linux] name_template: "gh_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: true format: tar.gz rlcp: true files: - LICENSE - ./share/man/man1/gh*.1 - id: macos-archive builds: [macos] name_template: "gh_{{ .Version }}_macOS_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: true format: zip rlcp: true files: - LICENSE - ./share/man/man1/gh*.1 - id: windows-archive builds: [windows] name_template: "gh_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" wrap_in_directory: false format: zip rlcp: true files: - LICENSE nfpms: #build:linux - license: MIT maintainer: GitHub homepage: https://github.com/cli/cli bindir: /usr dependencies: - git description: GitHub’s official command line tool. formats: - deb - rpm contents: - src: "./share/man/man1/gh*.1" dst: "/usr/share/man/man1" - src: "./share/bash-completion/completions/gh" dst: "/usr/share/bash-completion/completions/gh" - src: "./share/fish/vendor_completions.d/gh.fish" dst: "/usr/share/fish/vendor_completions.d/gh.fish" - src: "./share/zsh/site-functions/_gh" dst: "/usr/share/zsh/site-functions/_gh" cli-2.45.0/LICENSE000066400000000000000000000020541457137741500133750ustar00rootroot00000000000000MIT License Copyright (c) 2019 GitHub Inc. 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. cli-2.45.0/Makefile000066400000000000000000000057201457137741500140330ustar00rootroot00000000000000CGO_CPPFLAGS ?= ${CPPFLAGS} export CGO_CPPFLAGS CGO_CFLAGS ?= ${CFLAGS} export CGO_CFLAGS CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS}) export CGO_LDFLAGS EXE = ifeq ($(shell go env GOOS),windows) EXE = .exe endif ## The following tasks delegate to `script/build.go` so they can be run cross-platform. .PHONY: bin/gh$(EXE) bin/gh$(EXE): script/build$(EXE) @script/build$(EXE) $@ script/build$(EXE): script/build.go ifeq ($(EXE),) GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o $@ $< else go build -o $@ $< endif .PHONY: clean clean: script/build$(EXE) @$< $@ .PHONY: manpages manpages: script/build$(EXE) @$< $@ .PHONY: completions completions: bin/gh$(EXE) mkdir -p ./share/bash-completion/completions ./share/fish/vendor_completions.d ./share/zsh/site-functions bin/gh$(EXE) completion -s bash > ./share/bash-completion/completions/gh bin/gh$(EXE) completion -s fish > ./share/fish/vendor_completions.d/gh.fish bin/gh$(EXE) completion -s zsh > ./share/zsh/site-functions/_gh # just a convenience task around `go test` .PHONY: test test: go test ./... ## Site-related tasks are exclusively intended for use by the GitHub CLI team and for our release automation. site: git clone https://github.com/github/cli.github.com.git "$@" .PHONY: site-docs site-docs: site git -C site pull git -C site rm 'manual/gh*.md' 2>/dev/null || true go run ./cmd/gen-docs --website --doc-path site/manual rm -f site/manual/*.bak git -C site add 'manual/gh*.md' git -C site commit -m 'update help docs' || true .PHONY: site-bump site-bump: site-docs ifndef GITHUB_REF $(error GITHUB_REF is not set) endif sed -i.bak -E 's/(assign version = )".+"/\1"$(GITHUB_REF:refs/tags/v%=%)"/' site/index.html rm -f site/index.html.bak git -C site commit -m '$(GITHUB_REF:refs/tags/v%=%)' index.html ## Install/uninstall tasks are here for use on *nix platform. On Windows, there is no equivalent. DESTDIR := prefix := /usr/local bindir := ${prefix}/bin datadir := ${prefix}/share mandir := ${datadir}/man .PHONY: install install: bin/gh manpages completions install -d ${DESTDIR}${bindir} install -m755 bin/gh ${DESTDIR}${bindir}/ install -d ${DESTDIR}${mandir}/man1 install -m644 ./share/man/man1/* ${DESTDIR}${mandir}/man1/ install -d ${DESTDIR}${datadir}/bash-completion/completions install -m644 ./share/bash-completion/completions/gh ${DESTDIR}${datadir}/bash-completion/completions/gh install -d ${DESTDIR}${datadir}/fish/vendor_completions.d install -m644 ./share/fish/vendor_completions.d/gh.fish ${DESTDIR}${datadir}/fish/vendor_completions.d/gh.fish install -d ${DESTDIR}${datadir}/zsh/site-functions install -m644 ./share/zsh/site-functions/_gh ${DESTDIR}${datadir}/zsh/site-functions/_gh .PHONY: uninstall uninstall: rm -f ${DESTDIR}${bindir}/gh ${DESTDIR}${mandir}/man1/gh.1 ${DESTDIR}${mandir}/man1/gh-*.1 rm -f ${DESTDIR}${datadir}/bash-completion/completions/gh rm -f ${DESTDIR}${datadir}/fish/vendor_completions.d/gh.fish rm -f ${DESTDIR}${datadir}/zsh/site-functions/_gh cli-2.45.0/README.md000066400000000000000000000124721457137741500136540ustar00rootroot00000000000000# GitHub CLI `gh` is GitHub on the command line. It brings pull requests, issues, and other GitHub concepts to the terminal next to where you are already working with `git` and your code. ![screenshot of gh pr status](https://user-images.githubusercontent.com/98482/84171218-327e7a80-aa40-11ea-8cd1-5177fc2d0e72.png) GitHub CLI is supported for users on GitHub.com and GitHub Enterprise Server 2.20+ with support for macOS, Windows, and Linux. ## Documentation For [installation options see below](#installation), for usage instructions [see the manual][manual]. ## Contributing If anything feels off, or if you feel that some functionality is missing, please check out the [contributing page][contributing]. There you will find instructions for sharing your feedback, building the tool locally, and submitting pull requests to the project. If you are a hubber and are interested in shipping new commands for the CLI, check out our [doc on internal contributions][intake-doc]. ## Installation ### macOS `gh` is available via [Homebrew][], [MacPorts][], [Conda][], [Spack][], [Webi][], and as a downloadable binary from the [releases page][]. #### Homebrew | Install: | Upgrade: | | ----------------- | ----------------- | | `brew install gh` | `brew upgrade gh` | #### MacPorts | Install: | Upgrade: | | ---------------------- | ---------------------------------------------- | | `sudo port install gh` | `sudo port selfupdate && sudo port upgrade gh` | #### Conda | Install: | Upgrade: | |------------------------------------------|-----------------------------------------| | `conda install gh --channel conda-forge` | `conda update gh --channel conda-forge` | Additional Conda installation options available on the [gh-feedstock page](https://github.com/conda-forge/gh-feedstock#installing-gh). #### Spack | Install: | Upgrade: | | ------------------ | ---------------------------------------- | | `spack install gh` | `spack uninstall gh && spack install gh` | #### Webi | Install: | Upgrade: | | ----------------------------------- | ---------------- | | `curl -sS https://webi.sh/gh \| sh` | `webi gh@stable` | For more information about the Webi installer see [its homepage](https://webinstall.dev/). ### Linux & BSD `gh` is available via: - [our Debian and RPM repositories](./docs/install_linux.md); - community-maintained repositories in various Linux distros; - OS-agnostic package managers such as [Homebrew](#homebrew), [Conda](#conda), [Spack](#spack), [Webi](#webi); and - our [releases page][] as precompiled binaries. For more information, see [Linux & BSD installation](./docs/install_linux.md). ### Windows `gh` is available via [WinGet][], [scoop][], [Chocolatey][], [Conda](#conda), [Webi](#webi), and as downloadable MSI. #### WinGet | Install: | Upgrade: | | ------------------- | --------------------| | `winget install --id GitHub.cli` | `winget upgrade --id GitHub.cli` | > **Note** > The Windows installer modifies your PATH. When using Windows Terminal, you will need to **open a new window** for the changes to take effect. (Simply opening a new tab will _not_ be sufficient.) #### scoop | Install: | Upgrade: | | ------------------ | ------------------ | | `scoop install gh` | `scoop update gh` | #### Chocolatey | Install: | Upgrade: | | ------------------ | ------------------ | | `choco install gh` | `choco upgrade gh` | #### Signed MSI MSI installers are available for download on the [releases page][]. ### Codespaces To add GitHub CLI to your codespace, add the following to your [devcontainer file](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-features-to-a-devcontainer-file): ```json "features": { "ghcr.io/devcontainers/features/github-cli:1": {} } ``` ### GitHub Actions GitHub CLI comes pre-installed in all [GitHub-Hosted Runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners). ### Other platforms Download packaged binaries from the [releases page][]. ### Build from source See here on how to [build GitHub CLI from source][build from source]. ## Comparison with hub For many years, [hub][] was the unofficial GitHub CLI tool. `gh` is a new project that helps us explore what an official GitHub CLI tool can look like with a fundamentally different design. While both tools bring GitHub to the terminal, `hub` behaves as a proxy to `git`, and `gh` is a standalone tool. Check out our [more detailed explanation][gh-vs-hub] to learn more. [manual]: https://cli.github.com/manual/ [Homebrew]: https://brew.sh [MacPorts]: https://www.macports.org [winget]: https://github.com/microsoft/winget-cli [scoop]: https://scoop.sh [Chocolatey]: https://chocolatey.org [Conda]: https://docs.conda.io/en/latest/ [Spack]: https://spack.io [Webi]: https://webinstall.dev [releases page]: https://github.com/cli/cli/releases/latest [hub]: https://github.com/github/hub [contributing]: ./.github/CONTRIBUTING.md [gh-vs-hub]: ./docs/gh-vs-hub.md [build from source]: ./docs/source.md [intake-doc]: ./docs/working-with-us.md cli-2.45.0/api/000077500000000000000000000000001457137741500131405ustar00rootroot00000000000000cli-2.45.0/api/client.go000066400000000000000000000204301457137741500147440ustar00rootroot00000000000000package api import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "regexp" "strings" "github.com/cli/cli/v2/internal/ghinstance" ghAPI "github.com/cli/go-gh/v2/pkg/api" ) const ( accept = "Accept" authorization = "Authorization" cacheTTL = "X-GH-CACHE-TTL" graphqlFeatures = "GraphQL-Features" features = "merge_queue" userAgent = "User-Agent" ) var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) func NewClientFromHTTP(httpClient *http.Client) *Client { client := &Client{http: httpClient} return client } type Client struct { http *http.Client } func (c *Client) HTTP() *http.Client { return c.http } type GraphQLError struct { *ghAPI.GraphQLError } type HTTPError struct { *ghAPI.HTTPError scopesSuggestion string } func (err HTTPError) ScopesSuggestion() string { return err.scopesSuggestion } // GraphQL performs a GraphQL request using the query string and parses the response into data receiver. If there are errors in the response, // GraphQLError will be returned, but the receiver will also be partially populated. func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error { opts := clientOptions(hostname, c.http.Transport) opts.Headers[graphqlFeatures] = features gqlClient, err := ghAPI.NewGraphQLClient(opts) if err != nil { return err } return handleResponse(gqlClient.Do(query, variables, data)) } // Mutate performs a GraphQL mutation based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, // GraphQLError will be returned, but the receiver will also be partially populated. func (c Client) Mutate(hostname, name string, mutation interface{}, variables map[string]interface{}) error { opts := clientOptions(hostname, c.http.Transport) opts.Headers[graphqlFeatures] = features gqlClient, err := ghAPI.NewGraphQLClient(opts) if err != nil { return err } return handleResponse(gqlClient.Mutate(name, mutation, variables)) } // Query performs a GraphQL query based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, // GraphQLError will be returned, but the receiver will also be partially populated. func (c Client) Query(hostname, name string, query interface{}, variables map[string]interface{}) error { opts := clientOptions(hostname, c.http.Transport) opts.Headers[graphqlFeatures] = features gqlClient, err := ghAPI.NewGraphQLClient(opts) if err != nil { return err } return handleResponse(gqlClient.Query(name, query, variables)) } // QueryWithContext performs a GraphQL query based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, // GraphQLError will be returned, but the receiver will also be partially populated. func (c Client) QueryWithContext(ctx context.Context, hostname, name string, query interface{}, variables map[string]interface{}) error { opts := clientOptions(hostname, c.http.Transport) opts.Headers[graphqlFeatures] = features gqlClient, err := ghAPI.NewGraphQLClient(opts) if err != nil { return err } return handleResponse(gqlClient.QueryWithContext(ctx, name, query, variables)) } // REST performs a REST request and parses the response. func (c Client) REST(hostname string, method string, p string, body io.Reader, data interface{}) error { opts := clientOptions(hostname, c.http.Transport) restClient, err := ghAPI.NewRESTClient(opts) if err != nil { return err } return handleResponse(restClient.Do(method, p, body, data)) } func (c Client) RESTWithNext(hostname string, method string, p string, body io.Reader, data interface{}) (string, error) { opts := clientOptions(hostname, c.http.Transport) restClient, err := ghAPI.NewRESTClient(opts) if err != nil { return "", err } resp, err := restClient.Request(method, p, body) if err != nil { return "", err } defer resp.Body.Close() success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { return "", HandleHTTPError(resp) } if resp.StatusCode == http.StatusNoContent { return "", nil } b, err := io.ReadAll(resp.Body) if err != nil { return "", err } err = json.Unmarshal(b, &data) if err != nil { return "", err } var next string for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) { if len(m) > 2 && m[2] == "next" { next = m[1] } } return next, nil } // HandleHTTPError parses a http.Response into a HTTPError. func HandleHTTPError(resp *http.Response) error { return handleResponse(ghAPI.HandleHTTPError(resp)) } // handleResponse takes a ghAPI.HTTPError or ghAPI.GraphQLError and converts it into an // HTTPError or GraphQLError respectively. func handleResponse(err error) error { if err == nil { return nil } var restErr *ghAPI.HTTPError if errors.As(err, &restErr) { return HTTPError{ HTTPError: restErr, scopesSuggestion: generateScopesSuggestion(restErr.StatusCode, restErr.Headers.Get("X-Accepted-Oauth-Scopes"), restErr.Headers.Get("X-Oauth-Scopes"), restErr.RequestURL.Hostname()), } } var gqlErr *ghAPI.GraphQLError if errors.As(err, &gqlErr) { return GraphQLError{ GraphQLError: gqlErr, } } return err } // ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth // scopes in case a server response indicates that there are missing scopes. func ScopesSuggestion(resp *http.Response) string { return generateScopesSuggestion(resp.StatusCode, resp.Header.Get("X-Accepted-Oauth-Scopes"), resp.Header.Get("X-Oauth-Scopes"), resp.Request.URL.Hostname()) } // EndpointNeedsScopes adds additional OAuth scopes to an HTTP response as if they were returned from the // server endpoint. This improves HTTP 4xx error messaging for endpoints that don't explicitly list the // OAuth scopes they need. func EndpointNeedsScopes(resp *http.Response, s string) *http.Response { if resp.StatusCode >= 400 && resp.StatusCode < 500 { oldScopes := resp.Header.Get("X-Accepted-Oauth-Scopes") resp.Header.Set("X-Accepted-Oauth-Scopes", fmt.Sprintf("%s, %s", oldScopes, s)) } return resp } func generateScopesSuggestion(statusCode int, endpointNeedsScopes, tokenHasScopes, hostname string) string { if statusCode < 400 || statusCode > 499 || statusCode == 422 { return "" } if tokenHasScopes == "" { return "" } gotScopes := map[string]struct{}{} for _, s := range strings.Split(tokenHasScopes, ",") { s = strings.TrimSpace(s) gotScopes[s] = struct{}{} // Certain scopes may be grouped under a single "top-level" scope. The following branch // statements include these grouped/implied scopes when the top-level scope is encountered. // See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps. if s == "repo" { gotScopes["repo:status"] = struct{}{} gotScopes["repo_deployment"] = struct{}{} gotScopes["public_repo"] = struct{}{} gotScopes["repo:invite"] = struct{}{} gotScopes["security_events"] = struct{}{} } else if s == "user" { gotScopes["read:user"] = struct{}{} gotScopes["user:email"] = struct{}{} gotScopes["user:follow"] = struct{}{} } else if s == "codespace" { gotScopes["codespace:secrets"] = struct{}{} } else if strings.HasPrefix(s, "admin:") { gotScopes["read:"+strings.TrimPrefix(s, "admin:")] = struct{}{} gotScopes["write:"+strings.TrimPrefix(s, "admin:")] = struct{}{} } else if strings.HasPrefix(s, "write:") { gotScopes["read:"+strings.TrimPrefix(s, "write:")] = struct{}{} } } for _, s := range strings.Split(endpointNeedsScopes, ",") { s = strings.TrimSpace(s) if _, gotScope := gotScopes[s]; s == "" || gotScope { continue } return fmt.Sprintf( "This API operation needs the %[1]q scope. To request it, run: gh auth refresh -h %[2]s -s %[1]s", s, ghinstance.NormalizeHostname(hostname), ) } return "" } func clientOptions(hostname string, transport http.RoundTripper) ghAPI.ClientOptions { // AuthToken, and Headers are being handled by transport, // so let go-gh know that it does not need to resolve them. opts := ghAPI.ClientOptions{ AuthToken: "none", Headers: map[string]string{ authorization: "", }, Host: hostname, SkipDefaultHeaders: true, Transport: transport, LogIgnoreEnv: true, } return opts } cli-2.45.0/api/client_test.go000066400000000000000000000156221457137741500160120ustar00rootroot00000000000000package api import ( "bytes" "errors" "io" "net/http" "net/http/httptest" "testing" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" ) func newTestClient(reg *httpmock.Registry) *Client { client := &http.Client{} httpmock.ReplaceTripper(client, reg) return NewClientFromHTTP(client) } func TestGraphQL(t *testing.T) { http := &httpmock.Registry{} client := newTestClient(http) vars := map[string]interface{}{"name": "Mona"} response := struct { Viewer struct { Login string } }{} http.Register( httpmock.GraphQL("QUERY"), httpmock.StringResponse(`{"data":{"viewer":{"login":"hubot"}}}`), ) err := client.GraphQL("github.com", "QUERY", vars, &response) assert.NoError(t, err) assert.Equal(t, "hubot", response.Viewer.Login) req := http.Requests[0] reqBody, _ := io.ReadAll(req.Body) assert.Equal(t, `{"query":"QUERY","variables":{"name":"Mona"}}`, string(reqBody)) } func TestGraphQLError(t *testing.T) { reg := &httpmock.Registry{} client := newTestClient(reg) response := struct{}{} reg.Register( httpmock.GraphQL(""), httpmock.StringResponse(` { "errors": [ { "type": "NOT_FOUND", "message": "OH NO", "path": ["repository", "issue"] }, { "type": "ACTUALLY_ITS_FINE", "message": "this is fine", "path": ["repository", "issues", 0, "comments"] } ] } `), ) err := client.GraphQL("github.com", "", nil, &response) if err == nil || err.Error() != "GraphQL: OH NO (repository.issue), this is fine (repository.issues.0.comments)" { t.Fatalf("got %q", err.Error()) } } func TestRESTGetDelete(t *testing.T) { http := &httpmock.Registry{} client := newTestClient(http) http.Register( httpmock.REST("DELETE", "applications/CLIENTID/grant"), httpmock.StatusStringResponse(204, "{}"), ) r := bytes.NewReader([]byte(`{}`)) err := client.REST("github.com", "DELETE", "applications/CLIENTID/grant", r, nil) assert.NoError(t, err) } func TestRESTWithFullURL(t *testing.T) { http := &httpmock.Registry{} client := newTestClient(http) http.Register( httpmock.REST("GET", "api/v3/user/repos"), httpmock.StatusStringResponse(200, "{}")) http.Register( httpmock.REST("GET", "user/repos"), httpmock.StatusStringResponse(200, "{}")) err := client.REST("example.com", "GET", "user/repos", nil, nil) assert.NoError(t, err) err = client.REST("example.com", "GET", "https://another.net/user/repos", nil, nil) assert.NoError(t, err) assert.Equal(t, "example.com", http.Requests[0].URL.Hostname()) assert.Equal(t, "another.net", http.Requests[1].URL.Hostname()) } func TestRESTError(t *testing.T) { fakehttp := &httpmock.Registry{} client := newTestClient(fakehttp) fakehttp.Register(httpmock.MatchAny, func(req *http.Request) (*http.Response, error) { return &http.Response{ Request: req, StatusCode: 422, Body: io.NopCloser(bytes.NewBufferString(`{"message": "OH NO"}`)), Header: map[string][]string{ "Content-Type": {"application/json; charset=utf-8"}, }, }, nil }) var httpErr HTTPError err := client.REST("github.com", "DELETE", "repos/branch", nil, nil) if err == nil || !errors.As(err, &httpErr) { t.Fatalf("got %v", err) } if httpErr.StatusCode != 422 { t.Errorf("expected status code 422, got %d", httpErr.StatusCode) } if httpErr.Error() != "HTTP 422: OH NO (https://api.github.com/repos/branch)" { t.Errorf("got %q", httpErr.Error()) } } func TestHandleHTTPError_GraphQL502(t *testing.T) { req, err := http.NewRequest("GET", "https://api.github.com/user", nil) if err != nil { t.Fatal(err) } resp := &http.Response{ Request: req, StatusCode: 502, Body: io.NopCloser(bytes.NewBufferString(`{ "data": null, "errors": [{ "message": "Something went wrong" }] }`)), Header: map[string][]string{"Content-Type": {"application/json"}}, } err = HandleHTTPError(resp) if err == nil || err.Error() != "HTTP 502: Something went wrong (https://api.github.com/user)" { t.Errorf("got error: %v", err) } } func TestHTTPError_ScopesSuggestion(t *testing.T) { makeResponse := func(s int, u, haveScopes, needScopes string) *http.Response { req, err := http.NewRequest("GET", u, nil) if err != nil { t.Fatal(err) } return &http.Response{ Request: req, StatusCode: s, Body: io.NopCloser(bytes.NewBufferString(`{}`)), Header: map[string][]string{ "Content-Type": {"application/json"}, "X-Oauth-Scopes": {haveScopes}, "X-Accepted-Oauth-Scopes": {needScopes}, }, } } tests := []struct { name string resp *http.Response want string }{ { name: "has necessary scopes", resp: makeResponse(404, "https://api.github.com/gists", "repo, gist, read:org", "gist"), want: ``, }, { name: "normalizes scopes", resp: makeResponse(404, "https://api.github.com/orgs/ORG/discussions", "admin:org, write:discussion", "read:org, read:discussion"), want: ``, }, { name: "no scopes on endpoint", resp: makeResponse(404, "https://api.github.com/user", "repo", ""), want: ``, }, { name: "missing a scope", resp: makeResponse(404, "https://api.github.com/gists", "repo, read:org", "gist, delete_repo"), want: `This API operation needs the "gist" scope. To request it, run: gh auth refresh -h github.com -s gist`, }, { name: "server error", resp: makeResponse(500, "https://api.github.com/gists", "repo", "gist"), want: ``, }, { name: "no scopes on token", resp: makeResponse(404, "https://api.github.com/gists", "", "gist, delete_repo"), want: ``, }, { name: "http code is 422", resp: makeResponse(422, "https://api.github.com/gists", "", "gist"), want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { httpError := HandleHTTPError(tt.resp) if got := httpError.(HTTPError).ScopesSuggestion(); got != tt.want { t.Errorf("HTTPError.ScopesSuggestion() = %v, want %v", got, tt.want) } }) } } func TestHTTPHeaders(t *testing.T) { var gotReq *http.Request ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotReq = r w.WriteHeader(http.StatusNoContent) })) defer ts.Close() ios, _, _, stderr := iostreams.Test() httpClient, err := NewHTTPClient(HTTPClientOptions{ AppVersion: "v1.2.3", Config: tinyConfig{ts.URL[7:] + ":oauth_token": "MYTOKEN"}, Log: ios.ErrOut, }) assert.NoError(t, err) client := NewClientFromHTTP(httpClient) err = client.REST(ts.URL, "GET", ts.URL+"/user/repos", nil, nil) assert.NoError(t, err) wantHeader := map[string]string{ "Accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", "Authorization": "token MYTOKEN", "Content-Type": "application/json; charset=utf-8", "User-Agent": "GitHub CLI v1.2.3", } for name, value := range wantHeader { assert.Equal(t, value, gotReq.Header.Get(name), name) } assert.Equal(t, "", stderr.String()) } cli-2.45.0/api/export_pr.go000066400000000000000000000102111457137741500155040ustar00rootroot00000000000000package api import ( "reflect" "strings" ) func (issue *Issue) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(issue).Elem() data := map[string]interface{}{} for _, f := range fields { switch f { case "comments": data[f] = issue.Comments.Nodes case "assignees": data[f] = issue.Assignees.Nodes case "labels": data[f] = issue.Labels.Nodes case "projectCards": data[f] = issue.ProjectCards.Nodes case "projectItems": items := make([]map[string]interface{}, 0, len(issue.ProjectItems.Nodes)) for _, n := range issue.ProjectItems.Nodes { items = append(items, map[string]interface{}{ "status": n.Status, "title": n.Project.Title, }) } data[f] = items default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func (pr *PullRequest) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(pr).Elem() data := map[string]interface{}{} for _, f := range fields { switch f { case "headRepository": data[f] = pr.HeadRepository case "statusCheckRollup": if n := pr.StatusCheckRollup.Nodes; len(n) > 0 { checks := make([]interface{}, 0, len(n[0].Commit.StatusCheckRollup.Contexts.Nodes)) for _, c := range n[0].Commit.StatusCheckRollup.Contexts.Nodes { if c.TypeName == "CheckRun" { checks = append(checks, map[string]interface{}{ "__typename": c.TypeName, "name": c.Name, "workflowName": c.CheckSuite.WorkflowRun.Workflow.Name, "status": c.Status, "conclusion": c.Conclusion, "startedAt": c.StartedAt, "completedAt": c.CompletedAt, "detailsUrl": c.DetailsURL, }) } else { checks = append(checks, map[string]interface{}{ "__typename": c.TypeName, "context": c.Context, "state": c.State, "targetUrl": c.TargetURL, "startedAt": c.CreatedAt, }) } } data[f] = checks } else { data[f] = nil } case "commits": commits := make([]interface{}, 0, len(pr.Commits.Nodes)) for _, c := range pr.Commits.Nodes { commit := c.Commit authors := make([]interface{}, 0, len(commit.Authors.Nodes)) for _, author := range commit.Authors.Nodes { authors = append(authors, map[string]interface{}{ "name": author.Name, "email": author.Email, "id": author.User.ID, "login": author.User.Login, }) } commits = append(commits, map[string]interface{}{ "oid": commit.OID, "messageHeadline": commit.MessageHeadline, "messageBody": commit.MessageBody, "committedDate": commit.CommittedDate, "authoredDate": commit.AuthoredDate, "authors": authors, }) } data[f] = commits case "comments": data[f] = pr.Comments.Nodes case "assignees": data[f] = pr.Assignees.Nodes case "labels": data[f] = pr.Labels.Nodes case "projectCards": data[f] = pr.ProjectCards.Nodes case "projectItems": items := make([]map[string]interface{}, 0, len(pr.ProjectItems.Nodes)) for _, n := range pr.ProjectItems.Nodes { items = append(items, map[string]interface{}{ "status": n.Status, "title": n.Project.Title, }) } data[f] = items case "reviews": data[f] = pr.Reviews.Nodes case "latestReviews": data[f] = pr.LatestReviews.Nodes case "files": data[f] = pr.Files.Nodes case "reviewRequests": requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes)) for _, req := range pr.ReviewRequests.Nodes { r := req.RequestedReviewer switch r.TypeName { case "User": requests = append(requests, map[string]string{ "__typename": r.TypeName, "login": r.Login, }) case "Team": requests = append(requests, map[string]string{ "__typename": r.TypeName, "name": r.Name, "slug": r.LoginOrSlug(), }) } } data[f] = &requests default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func fieldByName(v reflect.Value, field string) reflect.Value { return v.FieldByNameFunc(func(s string) bool { return strings.EqualFold(field, s) }) } cli-2.45.0/api/export_pr_test.go000066400000000000000000000132141457137741500165510ustar00rootroot00000000000000package api import ( "bytes" "encoding/json" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestIssue_ExportData(t *testing.T) { tests := []struct { name string fields []string inputJSON string outputJSON string }{ { name: "simple", fields: []string{"number", "title"}, inputJSON: heredoc.Doc(` { "title": "Bugs hugs", "number": 2345 } `), outputJSON: heredoc.Doc(` { "number": 2345, "title": "Bugs hugs" } `), }, { name: "milestone", fields: []string{"number", "milestone"}, inputJSON: heredoc.Doc(` { "number": 2345, "milestone": {"title": "The next big thing"} } `), outputJSON: heredoc.Doc(` { "milestone": { "number": 0, "title": "The next big thing", "description": "", "dueOn": null }, "number": 2345 } `), }, { name: "project cards", fields: []string{"projectCards"}, inputJSON: heredoc.Doc(` { "projectCards": { "nodes": [ { "project": { "name": "Rewrite" }, "column": { "name": "TO DO" } } ] } } `), outputJSON: heredoc.Doc(` { "projectCards": [ { "project": { "name": "Rewrite" }, "column": { "name": "TO DO" } } ] } `), }, { name: "project items", fields: []string{"projectItems"}, inputJSON: heredoc.Doc(` { "projectItems": { "nodes": [ { "id": "PVTI_id", "project": { "id": "PVT_id", "title": "Some Project" }, "status": { "name": "Todo", "optionId": "abc123" } } ] } } `), outputJSON: heredoc.Doc(` { "projectItems": [ { "status": { "optionId": "abc123", "name": "Todo" }, "title": "Some Project" } ] } `), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var issue Issue dec := json.NewDecoder(strings.NewReader(tt.inputJSON)) require.NoError(t, dec.Decode(&issue)) exported := issue.ExportData(tt.fields) buf := bytes.Buffer{} enc := json.NewEncoder(&buf) enc.SetIndent("", "\t") require.NoError(t, enc.Encode(exported)) assert.Equal(t, tt.outputJSON, buf.String()) }) } } func TestPullRequest_ExportData(t *testing.T) { tests := []struct { name string fields []string inputJSON string outputJSON string }{ { name: "simple", fields: []string{"number", "title"}, inputJSON: heredoc.Doc(` { "title": "Bugs hugs", "number": 2345 } `), outputJSON: heredoc.Doc(` { "number": 2345, "title": "Bugs hugs" } `), }, { name: "milestone", fields: []string{"number", "milestone"}, inputJSON: heredoc.Doc(` { "number": 2345, "milestone": {"title": "The next big thing"} } `), outputJSON: heredoc.Doc(` { "milestone": { "number": 0, "title": "The next big thing", "description": "", "dueOn": null }, "number": 2345 } `), }, { name: "status checks", fields: []string{"statusCheckRollup"}, inputJSON: heredoc.Doc(` { "statusCheckRollup": { "nodes": [ { "commit": { "statusCheckRollup": { "contexts": { "nodes": [ { "__typename": "CheckRun", "name": "mycheck", "checkSuite": {"workflowRun": {"workflow": {"name": "myworkflow"}}}, "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2020-08-31T15:44:24+02:00", "completedAt": "2020-08-31T15:45:24+02:00", "detailsUrl": "http://example.com/details" }, { "__typename": "StatusContext", "context": "mycontext", "state": "SUCCESS", "createdAt": "2020-08-31T15:44:24+02:00", "targetUrl": "http://example.com/details" } ] } } } } ] } } `), outputJSON: heredoc.Doc(` { "statusCheckRollup": [ { "__typename": "CheckRun", "name": "mycheck", "workflowName": "myworkflow", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2020-08-31T15:44:24+02:00", "completedAt": "2020-08-31T15:45:24+02:00", "detailsUrl": "http://example.com/details" }, { "__typename": "StatusContext", "context": "mycontext", "state": "SUCCESS", "startedAt": "2020-08-31T15:44:24+02:00", "targetUrl": "http://example.com/details" } ] } `), }, { name: "project items", fields: []string{"projectItems"}, inputJSON: heredoc.Doc(` { "projectItems": { "nodes": [ { "id": "PVTPR_id", "project": { "id": "PVT_id", "title": "Some Project" }, "status": { "name": "Todo", "optionId": "abc123" } } ] } } `), outputJSON: heredoc.Doc(` { "projectItems": [ { "status": { "optionId": "abc123", "name": "Todo" }, "title": "Some Project" } ] } `), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var pr PullRequest dec := json.NewDecoder(strings.NewReader(tt.inputJSON)) require.NoError(t, dec.Decode(&pr)) exported := pr.ExportData(tt.fields) buf := bytes.Buffer{} enc := json.NewEncoder(&buf) enc.SetIndent("", "\t") require.NoError(t, enc.Encode(exported)) var gotData interface{} dec = json.NewDecoder(&buf) require.NoError(t, dec.Decode(&gotData)) var expectData interface{} require.NoError(t, json.Unmarshal([]byte(tt.outputJSON), &expectData)) assert.Equal(t, expectData, gotData) }) } } cli-2.45.0/api/export_repo.go000066400000000000000000000022021457137741500160310ustar00rootroot00000000000000package api import ( "reflect" ) func (repo *Repository) ExportData(fields []string) map[string]interface{} { v := reflect.ValueOf(repo).Elem() data := map[string]interface{}{} for _, f := range fields { switch f { case "parent": data[f] = miniRepoExport(repo.Parent) case "templateRepository": data[f] = miniRepoExport(repo.TemplateRepository) case "languages": data[f] = repo.Languages.Edges case "labels": data[f] = repo.Labels.Nodes case "assignableUsers": data[f] = repo.AssignableUsers.Nodes case "mentionableUsers": data[f] = repo.MentionableUsers.Nodes case "milestones": data[f] = repo.Milestones.Nodes case "projects": data[f] = repo.Projects.Nodes case "repositoryTopics": var topics []RepositoryTopic for _, n := range repo.RepositoryTopics.Nodes { topics = append(topics, n.Topic) } data[f] = topics default: sf := fieldByName(v, f) data[f] = sf.Interface() } } return data } func miniRepoExport(r *Repository) map[string]interface{} { if r == nil { return nil } return map[string]interface{}{ "id": r.ID, "name": r.Name, "owner": r.Owner, } } cli-2.45.0/api/http_client.go000066400000000000000000000077031457137741500160130ustar00rootroot00000000000000package api import ( "fmt" "io" "net/http" "strings" "time" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/utils" ghAPI "github.com/cli/go-gh/v2/pkg/api" ) type tokenGetter interface { ActiveToken(string) (string, string) } type HTTPClientOptions struct { AppVersion string CacheTTL time.Duration Config tokenGetter EnableCache bool Log io.Writer LogColorize bool LogVerboseHTTP bool } func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) { // Provide invalid host, and token values so gh.HTTPClient will not automatically resolve them. // The real host and token are inserted at request time. clientOpts := ghAPI.ClientOptions{ Host: "none", AuthToken: "none", LogIgnoreEnv: true, } debugEnabled, debugValue := utils.IsDebugEnabled() if strings.Contains(debugValue, "api") { opts.LogVerboseHTTP = true } if opts.LogVerboseHTTP || debugEnabled { clientOpts.Log = opts.Log clientOpts.LogColorize = opts.LogColorize clientOpts.LogVerboseHTTP = opts.LogVerboseHTTP } headers := map[string]string{ userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), } clientOpts.Headers = headers if opts.EnableCache { clientOpts.EnableCache = opts.EnableCache clientOpts.CacheTTL = opts.CacheTTL } client, err := ghAPI.NewHTTPClient(clientOpts) if err != nil { return nil, err } if opts.Config != nil { client.Transport = AddAuthTokenHeader(client.Transport, opts.Config) } return client, nil } func NewCachedHTTPClient(httpClient *http.Client, ttl time.Duration) *http.Client { newClient := *httpClient newClient.Transport = AddCacheTTLHeader(httpClient.Transport, ttl) return &newClient } // AddCacheTTLHeader adds an header to the request telling the cache that the request // should be cached for a specified amount of time. func AddCacheTTLHeader(rt http.RoundTripper, ttl time.Duration) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { // If the header is already set in the request, don't overwrite it. if req.Header.Get(cacheTTL) == "" { req.Header.Set(cacheTTL, ttl.String()) } return rt.RoundTrip(req) }} } // AddAuthToken adds an authentication token header for the host specified by the request. func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { // If the header is already set in the request, don't overwrite it. if req.Header.Get(authorization) == "" { var redirectHostnameChange bool if req.Response != nil && req.Response.Request != nil { redirectHostnameChange = getHost(req) != getHost(req.Response.Request) } // Only set header if an initial request or redirect request to the same host as the initial request. // If the host has changed during a redirect do not add the authentication token header. if !redirectHostnameChange { hostname := ghinstance.NormalizeHostname(getHost(req)) if token, _ := cfg.ActiveToken(hostname); token != "" { req.Header.Set(authorization, fmt.Sprintf("token %s", token)) } } } return rt.RoundTrip(req) }} } // ExtractHeader extracts a named header from any response received by this client and, // if non-blank, saves it to dest. func ExtractHeader(name string, dest *string) func(http.RoundTripper) http.RoundTripper { return func(tr http.RoundTripper) http.RoundTripper { return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { res, err := tr.RoundTrip(req) if err == nil { if value := res.Header.Get(name); value != "" { *dest = value } } return res, err }} } } type funcTripper struct { roundTrip func(*http.Request) (*http.Response, error) } func (tr funcTripper) RoundTrip(req *http.Request) (*http.Response, error) { return tr.roundTrip(req) } func getHost(r *http.Request) string { if r.Host != "" { return r.Host } return r.URL.Host } cli-2.45.0/api/http_client_test.go000066400000000000000000000222251457137741500170460ustar00rootroot00000000000000package api import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "regexp" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewHTTPClient(t *testing.T) { type args struct { config tokenGetter appVersion string logVerboseHTTP bool } tests := []struct { name string args args host string wantHeader map[string]string wantStderr string }{ { name: "github.com", args: args{ config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, appVersion: "v1.2.3", logVerboseHTTP: false, }, host: "github.com", wantHeader: map[string]string{ "authorization": "token MYTOKEN", "user-agent": "GitHub CLI v1.2.3", "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", }, wantStderr: "", }, { name: "GHES", args: args{ config: tinyConfig{"example.com:oauth_token": "GHETOKEN"}, appVersion: "v1.2.3", }, host: "example.com", wantHeader: map[string]string{ "authorization": "token GHETOKEN", "user-agent": "GitHub CLI v1.2.3", "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", }, wantStderr: "", }, { name: "github.com no authentication token", args: args{ config: tinyConfig{"example.com:oauth_token": "MYTOKEN"}, appVersion: "v1.2.3", logVerboseHTTP: false, }, host: "github.com", wantHeader: map[string]string{ "authorization": "", "user-agent": "GitHub CLI v1.2.3", "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", }, wantStderr: "", }, { name: "GHES no authentication token", args: args{ config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, appVersion: "v1.2.3", logVerboseHTTP: false, }, host: "example.com", wantHeader: map[string]string{ "authorization": "", "user-agent": "GitHub CLI v1.2.3", "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", }, wantStderr: "", }, { name: "github.com in verbose mode", args: args{ config: tinyConfig{"github.com:oauth_token": "MYTOKEN"}, appVersion: "v1.2.3", logVerboseHTTP: true, }, host: "github.com", wantHeader: map[string]string{ "authorization": "token MYTOKEN", "user-agent": "GitHub CLI v1.2.3", "accept": "application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview", }, wantStderr: heredoc.Doc(` * Request at