Merge remote-tracking branch 'origin/trunk' into fix/error-on-no-browser
This commit is contained in:
commit
a889bfab20
272 changed files with 23509 additions and 3667 deletions
0
CODEOWNERS → .github/CODEOWNERS
vendored
0
CODEOWNERS → .github/CODEOWNERS
vendored
346
.github/workflows/deployment.yml
vendored
Normal file
346
.github/workflows/deployment.yml
vendored
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
name: Deployment
|
||||
|
||||
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.19"
|
||||
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@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ inputs.go_version }}
|
||||
- name: Install GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
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@v3
|
||||
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@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
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@v4
|
||||
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@v3
|
||||
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@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ inputs.go_version }}
|
||||
- name: Obtain signing certificate
|
||||
id: obtain_cert
|
||||
if: inputs.environment == 'production'
|
||||
shell: bash
|
||||
run: |
|
||||
base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx
|
||||
printf "cert-file=%s\n" ".\\cert.pfx" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
|
||||
- name: Install GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
version: "~1.17.1"
|
||||
install-only: true
|
||||
- name: Build release binaries
|
||||
shell: bash
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag_name }}
|
||||
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
|
||||
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
|
||||
run: script/release --local "$TAG_NAME" --platform windows
|
||||
- name: Set up MSBuild
|
||||
id: setupmsbuild
|
||||
uses: microsoft/setup-msbuild@v1.3.1
|
||||
- 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
|
||||
if: inputs.environment == 'production'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Get-ChildItem -Path .\dist -Filter *.msi | ForEach-Object {
|
||||
.\script\sign $_.FullName
|
||||
}
|
||||
env:
|
||||
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
|
||||
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
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]
|
||||
if: inputs.release
|
||||
steps:
|
||||
- name: Checkout cli/cli
|
||||
uses: actions/checkout@v3
|
||||
- name: Merge built artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Checkout documentation site
|
||||
uses: actions/checkout@v3
|
||||
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@v2
|
||||
if: inputs.environment == 'production' && !contains(inputs.tag_name, '-')
|
||||
with:
|
||||
formula-name: gh
|
||||
tag-name: ${{ inputs.tag_name }}
|
||||
push-to: cli/homebrew-core
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.HOMEBREW_PR_PAT }}
|
||||
222
.github/workflows/releases.yml
vendored
222
.github/workflows/releases.yml
vendored
|
|
@ -1,222 +0,0 @@
|
|||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write # publishing releases
|
||||
repository-projects: write # move cards between columns
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go 1.19
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
echo "tag-name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
gh api repos/$GITHUB_REPOSITORY/releases/generate-notes \
|
||||
-f tag_name="${GITHUB_REF#refs/tags/}" \
|
||||
-f target_commitish=trunk \
|
||||
-q .body > CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Install osslsigncode
|
||||
run: sudo apt-get install -y osslsigncode
|
||||
- name: Obtain signing cert
|
||||
run: |
|
||||
cert="$(mktemp -t cert.XXX)"
|
||||
base64 -d <<<"$CERT_CONTENTS" > "$cert"
|
||||
echo "CERT_FILE=$cert" >> $GITHUB_ENV
|
||||
env:
|
||||
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
version: "~1.13.1"
|
||||
args: release --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}}
|
||||
CERT_PASSWORD: ${{secrets.WINDOWS_CERT_PASSWORD}}
|
||||
- name: Checkout documentation site
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: github/cli.github.com
|
||||
path: site
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{secrets.SITE_SSH_KEY}}
|
||||
- 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
|
||||
run: make site-bump
|
||||
- name: Move project cards
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
PENDING_COLUMN: 8189733
|
||||
DONE_COLUMN: 7110130
|
||||
run: |
|
||||
api() { gh api -H 'accept: application/vnd.github.inertia-preview+json' "$@"; }
|
||||
api-write() { [[ $GITHUB_REF == *-* ]] && echo "skipping: api $*" || api "$@"; }
|
||||
cards=$(api --paginate projects/columns/$PENDING_COLUMN/cards | jq ".[].id")
|
||||
for card in $cards; do
|
||||
api-write --silent projects/columns/cards/$card/moves -f position=top -F column_id=$DONE_COLUMN
|
||||
done
|
||||
echo "moved ${#cards[@]} cards to the Done column"
|
||||
- name: Install packaging dependencies
|
||||
run: sudo apt-get install -y rpm reprepro
|
||||
- name: Set up GPG
|
||||
run: |
|
||||
echo "${{secrets.GPG_PUBKEY}}" | base64 -d | gpg --import --no-tty --batch --yes
|
||||
echo "${{secrets.GPG_KEY}}" | base64 -d | gpg --import --no-tty --batch --yes
|
||||
echo "allow-preset-passphrase" > ~/.gnupg/gpg-agent.conf
|
||||
gpg-connect-agent RELOADAGENT /bye
|
||||
echo "${{secrets.GPG_PASSPHRASE}}" | /usr/lib/gnupg2/gpg-preset-passphrase --preset "${{secrets.GPG_KEYGRIP}}"
|
||||
- name: Sign RPMs
|
||||
run: |
|
||||
cp script/rpmmacros ~/.rpmmacros
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Run createrepo
|
||||
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 --yes --detach-sign --armor repodata/repomd.xml
|
||||
popd
|
||||
- name: Run reprepro
|
||||
env:
|
||||
# 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
|
||||
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: Publish site
|
||||
env:
|
||||
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 ${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == *-* ]]; then
|
||||
git log --oneline @{upstream}..
|
||||
git diff --name-status @{upstream}..
|
||||
else
|
||||
git push
|
||||
fi
|
||||
|
||||
msi:
|
||||
needs: goreleaser
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Download gh.exe
|
||||
id: download_exe
|
||||
shell: bash
|
||||
run: |
|
||||
hub release download "${GITHUB_REF#refs/tags/}" -i '*windows_amd64*.zip'
|
||||
printf "zip=%s\n" *.zip >> $GITHUB_OUTPUT
|
||||
unzip -o *.zip && rm -v *.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Prepare PATH
|
||||
id: setupmsbuild
|
||||
uses: microsoft/setup-msbuild@v1.3.1
|
||||
- name: Build MSI
|
||||
id: buildmsi
|
||||
shell: bash
|
||||
env:
|
||||
ZIP_FILE: ${{ steps.download_exe.outputs.zip }}
|
||||
MSBUILD_PATH: ${{ steps.setupmsbuild.outputs.msbuildPath }}
|
||||
run: |
|
||||
name="$(basename "$ZIP_FILE" ".zip")"
|
||||
version="$(echo -e ${GITHUB_REF#refs/tags/v} | sed s/-.*$//)"
|
||||
"${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$PWD" -p:OutputPath="$PWD" -p:OutputName="$name" -p:ProductVersion="$version"
|
||||
- name: Obtain signing cert
|
||||
id: obtain_cert
|
||||
shell: bash
|
||||
run: |
|
||||
base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx
|
||||
printf "cert-file=%s\n" ".\\cert.pfx" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
|
||||
- name: Sign MSI
|
||||
env:
|
||||
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
|
||||
EXE_FILE: ${{ steps.buildmsi.outputs.msi }}
|
||||
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
|
||||
run: .\script\signtool sign /d "GitHub CLI" /f $env:CERT_FILE /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $env:EXE_FILE
|
||||
- name: Upload MSI
|
||||
shell: bash
|
||||
run: |
|
||||
tag_name="${GITHUB_REF#refs/tags/}"
|
||||
hub release edit "$tag_name" -m "" -a "$MSI_FILE"
|
||||
release_url="$(gh api repos/:owner/:repo/releases -q ".[]|select(.tag_name==\"${tag_name}\")|.url")"
|
||||
publish_args=( -F draft=false )
|
||||
if [[ $GITHUB_REF != *-* ]]; then
|
||||
publish_args+=( -f discussion_category_name="$DISCUSSION_CATEGORY" )
|
||||
fi
|
||||
gh api -X PATCH "$release_url" "${publish_args[@]}"
|
||||
env:
|
||||
MSI_FILE: ${{ steps.buildmsi.outputs.msi }}
|
||||
DISCUSSION_CATEGORY: General
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@v2
|
||||
if: "!contains(github.ref, '-')" # skip prereleases
|
||||
with:
|
||||
formula-name: gh
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }}
|
||||
- name: Checkout scoop bucket
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: cli/scoop-gh
|
||||
path: scoop-gh
|
||||
fetch-depth: 0
|
||||
token: ${{secrets.UPLOAD_GITHUB_TOKEN}}
|
||||
- name: Bump scoop bucket
|
||||
shell: bash
|
||||
run: |
|
||||
hub release download "${GITHUB_REF#refs/tags/}" -i '*_checksums.txt'
|
||||
script/scoop-gen "${GITHUB_REF#refs/tags/}" ./scoop-gh/gh.json < *_checksums.txt
|
||||
git -C ./scoop-gh commit -m "gh ${GITHUB_REF#refs/tags/}" gh.json
|
||||
if [[ $GITHUB_REF == *-* ]]; then
|
||||
git -C ./scoop-gh show -m
|
||||
else
|
||||
git -C ./scoop-gh push
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GIT_COMMITTER_NAME: cli automation
|
||||
GIT_AUTHOR_NAME: cli automation
|
||||
GIT_COMMITTER_EMAIL: noreply@github.com
|
||||
GIT_AUTHOR_EMAIL: noreply@github.com
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,12 +1,17 @@
|
|||
/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
|
||||
|
|
|
|||
|
|
@ -7,56 +7,76 @@ release:
|
|||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- make manpages GH_VERSION={{.Version}}
|
||||
- >-
|
||||
{{ if eq .Runtime.Goos "windows" }}echo{{ end }} make manpages GH_VERSION={{.Version}}
|
||||
- >-
|
||||
{{ if ne .Runtime.Goos "linux" }}echo{{ end }} make completions
|
||||
|
||||
builds:
|
||||
- <<: &build_defaults
|
||||
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: macos
|
||||
- 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"}}
|
||||
|
||||
- <<: *build_defaults
|
||||
id: linux
|
||||
- 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"}}
|
||||
|
||||
- <<: *build_defaults
|
||||
id: windows
|
||||
- id: windows #build:windows
|
||||
goos: [windows]
|
||||
goarch: [386, amd64, arm64]
|
||||
hooks:
|
||||
post:
|
||||
- cmd: ./script/sign-windows-executable.sh '{{ .Path }}'
|
||||
output: false
|
||||
- cmd: >-
|
||||
{{ if eq .Runtime.Goos "windows" }}.\script\sign{{ 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: nix
|
||||
builds: [macos, linux]
|
||||
<<: &archive_defaults
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
- id: linux-archive
|
||||
builds: [linux]
|
||||
name_template: "gh_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
wrap_in_directory: true
|
||||
replacements:
|
||||
darwin: macOS
|
||||
format: tar.gz
|
||||
rlcp: true
|
||||
files:
|
||||
- LICENSE
|
||||
- ./share/man/man1/gh*.1
|
||||
- id: windows
|
||||
- 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]
|
||||
<<: *archive_defaults
|
||||
name_template: "gh_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
wrap_in_directory: false
|
||||
format: zip
|
||||
rlcp: true
|
||||
files:
|
||||
- LICENSE
|
||||
|
||||
nfpms:
|
||||
nfpms: #build:linux
|
||||
- license: MIT
|
||||
maintainer: GitHub
|
||||
homepage: https://github.com/cli/cli
|
||||
|
|
@ -70,3 +90,9 @@ nfpms:
|
|||
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"
|
||||
|
|
|
|||
41
Makefile
41
Makefile
|
|
@ -6,26 +6,37 @@ CGO_LDFLAGS ?= $(filter -g -L% -l% -O%,${LDFLAGS})
|
|||
export CGO_LDFLAGS
|
||||
|
||||
EXE =
|
||||
ifeq ($(GOOS),windows)
|
||||
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
|
||||
@script/build $@
|
||||
bin/gh$(EXE): script/build$(EXE)
|
||||
@script/build$(EXE) $@
|
||||
|
||||
script/build: script/build.go
|
||||
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
|
||||
@script/build $@
|
||||
clean: script/build$(EXE)
|
||||
@$< $@
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: script/build
|
||||
@script/build $@
|
||||
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
|
||||
|
|
@ -60,15 +71,25 @@ endif
|
|||
DESTDIR :=
|
||||
prefix := /usr/local
|
||||
bindir := ${prefix}/bin
|
||||
mandir := ${prefix}/share/man
|
||||
datadir := ${prefix}/share
|
||||
mandir := ${datadir}/man
|
||||
|
||||
.PHONY: install
|
||||
install: bin/gh manpages
|
||||
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
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ For more information, see [Linux & BSD installation](./docs/install_linux.md).
|
|||
| `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 affect. (Simply opening a new tab will _not_ be sufficient.)
|
||||
> 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
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/go-gh"
|
||||
ghAPI "github.com/cli/go-gh/pkg/api"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -40,11 +39,11 @@ func (c *Client) HTTP() *http.Client {
|
|||
}
|
||||
|
||||
type GraphQLError struct {
|
||||
ghAPI.GQLError
|
||||
*ghAPI.GraphQLError
|
||||
}
|
||||
|
||||
type HTTPError struct {
|
||||
ghAPI.HTTPError
|
||||
*ghAPI.HTTPError
|
||||
scopesSuggestion string
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +56,7 @@ func (err HTTPError) ScopesSuggestion() string {
|
|||
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 := gh.GQLClient(&opts)
|
||||
gqlClient, err := ghAPI.NewGraphQLClient(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -69,7 +68,7 @@ func (c Client) GraphQL(hostname string, query string, variables map[string]inte
|
|||
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 := gh.GQLClient(&opts)
|
||||
gqlClient, err := ghAPI.NewGraphQLClient(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -81,7 +80,7 @@ func (c Client) Mutate(hostname, name string, mutation interface{}, variables ma
|
|||
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 := gh.GQLClient(&opts)
|
||||
gqlClient, err := ghAPI.NewGraphQLClient(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -93,7 +92,7 @@ func (c Client) Query(hostname, name string, query interface{}, variables map[st
|
|||
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 := gh.GQLClient(&opts)
|
||||
gqlClient, err := ghAPI.NewGraphQLClient(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -103,7 +102,7 @@ func (c Client) QueryWithContext(ctx context.Context, hostname, name string, que
|
|||
// 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 := gh.RESTClient(&opts)
|
||||
restClient, err := ghAPI.NewRESTClient(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -112,7 +111,7 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d
|
|||
|
||||
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 := gh.RESTClient(&opts)
|
||||
restClient, err := ghAPI.NewRESTClient(opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -157,14 +156,14 @@ func HandleHTTPError(resp *http.Response) error {
|
|||
return handleResponse(ghAPI.HandleHTTPError(resp))
|
||||
}
|
||||
|
||||
// handleResponse takes a ghAPI.HTTPError or ghAPI.GQLError and converts it into an
|
||||
// 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
|
||||
var restErr *ghAPI.HTTPError
|
||||
if errors.As(err, &restErr) {
|
||||
return HTTPError{
|
||||
HTTPError: restErr,
|
||||
|
|
@ -175,10 +174,10 @@ func handleResponse(err error) error {
|
|||
}
|
||||
}
|
||||
|
||||
var gqlErr ghAPI.GQLError
|
||||
var gqlErr *ghAPI.GraphQLError
|
||||
if errors.As(err, &gqlErr) {
|
||||
return GraphQLError{
|
||||
GQLError: gqlErr,
|
||||
GraphQLError: gqlErr,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/go-gh"
|
||||
ghAPI "github.com/cli/go-gh/pkg/api"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
)
|
||||
|
||||
type tokenGetter interface {
|
||||
|
|
@ -55,7 +54,7 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
|
|||
clientOpts.CacheTTL = opts.CacheTTL
|
||||
}
|
||||
|
||||
client, err := gh.HTTPClient(&clientOpts)
|
||||
client, err := ghAPI.NewHTTPClient(clientOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,42 +2,414 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_ChecksStatus(t *testing.T) {
|
||||
pr := PullRequest{}
|
||||
func TestChecksStatus_NoCheckRunsOrStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `
|
||||
{ "statusCheckRollup": { "nodes": [] } }
|
||||
`
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 0,
|
||||
Failing: 0,
|
||||
Passing: 0,
|
||||
Total: 0,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRuns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
expectedChecksStatus PullRequestChecksStatus
|
||||
}{
|
||||
{
|
||||
name: "QUEUED is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("QUEUED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "IN_PROGRESS is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("IN_PROGRESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "WAITING is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("WAITING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "PENDING is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("PENDING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "REQUESTED is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("REQUESTED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED with no conclusion is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("COMPLETED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / STARTUP_FAILURE is treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("STARTUP_FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / STALE is treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("STALE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / SUCCESS is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("SUCCESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / NEUTRAL is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("NEUTRAL"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / SKIPPED is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("SKIPPED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / ACTION_REQUIRED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("ACTION_REQUIRED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / TIMED_OUT is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("TIMED_OUT"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / CANCELLED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / CANCELLED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / FAILURE is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized Status are treated as Pending",
|
||||
payload: singleCheckRunWithStatus("AnUnrecognizedStatusJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized Conclusions are treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("AnUnrecognizedConclusionJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
|
||||
|
||||
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
expectedChecksStatus PullRequestChecksStatus
|
||||
}{
|
||||
{
|
||||
name: "EXPECTED is treated as Pending",
|
||||
payload: singleStatusContextWithState("EXPECTED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "PENDING is treated as Pending",
|
||||
payload: singleStatusContextWithState("PENDING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "SUCCESS is treated as Passing",
|
||||
payload: singleStatusContextWithState("SUCCESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "ERROR is treated as Failing",
|
||||
payload: singleStatusContextWithState("ERROR"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "FAILURE is treated as Failing",
|
||||
payload: singleStatusContextWithState("FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized States are treated as Pending",
|
||||
payload: singleStatusContextWithState("AnUnrecognizedStateJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
|
||||
|
||||
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRunsAndStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This might look a bit intimidating, but we're just inserting three nodes
|
||||
// into the rollup, two completed check run nodes and one status context node.
|
||||
payload := fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{ "state": "SUCCESS" },
|
||||
{ "state": "PENDING" },
|
||||
{ "state": "FAILURE" },
|
||||
{ "status": "IN_PROGRESS",
|
||||
"conclusion": null },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "SUCCESS" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "FAILURE" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "ACTION_REQUIRED" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "STALE" }
|
||||
%s,
|
||||
%s,
|
||||
%s
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`,
|
||||
completedCheckRunNode("SUCCESS"),
|
||||
statusContextNode("PENDING"),
|
||||
completedCheckRunNode("FAILURE"),
|
||||
)
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 1,
|
||||
Failing: 1,
|
||||
Passing: 1,
|
||||
Total: 3,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRunAndStatusContextCountsByState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 14,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "AnUnrecognizedStateJustForThisTest",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"statusContextCount": 6,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "AnUnrecognizedStateJustForThisTest",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`
|
||||
err := json.Unmarshal([]byte(payload), &pr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
assert.Equal(t, 8, checks.Total)
|
||||
assert.Equal(t, 3, checks.Pending)
|
||||
assert.Equal(t, 3, checks.Failing)
|
||||
assert.Equal(t, 2, checks.Passing)
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 11,
|
||||
Failing: 6,
|
||||
Passing: 4,
|
||||
Total: 20,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
// Note that it would be incorrect to provide a status of COMPLETED here
|
||||
// as the conclusion is always set to null. If you want a COMPLETED status,
|
||||
// use `singleCompletedCheckRunWithConclusion`.
|
||||
func singleCheckRunWithStatus(status string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "%s",
|
||||
"conclusion": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, status)
|
||||
}
|
||||
|
||||
func singleCompletedCheckRunWithConclusion(conclusion string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "COMPLETED",
|
||||
"conclusion": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, conclusion)
|
||||
}
|
||||
|
||||
func singleStatusContextWithState(state string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "StatusContext",
|
||||
"state": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, state)
|
||||
}
|
||||
|
||||
func completedCheckRunNode(conclusion string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "COMPLETED",
|
||||
"conclusion": "%s"
|
||||
}`, conclusion)
|
||||
}
|
||||
|
||||
func statusContextNode(state string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"__typename": "StatusContext",
|
||||
"state": "%s"
|
||||
}`, state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,63 +4,16 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
type LinkedBranch struct {
|
||||
ID string
|
||||
BranchName string
|
||||
RepoUrl string
|
||||
URL string
|
||||
}
|
||||
|
||||
// method to return url of linked branch, adds the branch name to the end of the repo url
|
||||
func (b *LinkedBranch) Url() string {
|
||||
return fmt.Sprintf("%s/tree/%s", b.RepoUrl, b.BranchName)
|
||||
}
|
||||
|
||||
func nameParam(params map[string]interface{}) string {
|
||||
if params["name"] != "" {
|
||||
return "name: $name,"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nameArg(params map[string]interface{}) string {
|
||||
if params["name"] != "" {
|
||||
return "$name: String, "
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func CreateBranchIssueReference(client *Client, repo *Repository, params map[string]interface{}) (*LinkedBranch, error) {
|
||||
query := fmt.Sprintf(`
|
||||
mutation CreateLinkedBranch($issueId: ID!, $oid: GitObjectID!, %[1]s$repositoryId: ID) {
|
||||
createLinkedBranch(input: {
|
||||
issueId: $issueId,
|
||||
%[2]s
|
||||
oid: $oid,
|
||||
repositoryId: $repositoryId
|
||||
}) {
|
||||
linkedBranch {
|
||||
id
|
||||
ref {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, nameArg(params), nameParam(params))
|
||||
|
||||
inputParams := map[string]interface{}{
|
||||
"repositoryId": repo.ID,
|
||||
}
|
||||
for key, val := range params {
|
||||
switch key {
|
||||
case "issueId", "name", "oid":
|
||||
inputParams[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
result := struct {
|
||||
func CreateLinkedBranch(client *Client, host string, repoID, issueID, branchID, branchName string) (string, error) {
|
||||
var mutation struct {
|
||||
CreateLinkedBranch struct {
|
||||
LinkedBranch struct {
|
||||
ID string
|
||||
|
|
@ -68,91 +21,76 @@ func CreateBranchIssueReference(client *Client, repo *Repository, params map[str
|
|||
Name string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
} `graphql:"createLinkedBranch(input: $input)"`
|
||||
}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, inputParams, &result)
|
||||
input := githubv4.CreateLinkedBranchInput{
|
||||
IssueID: githubv4.ID(issueID),
|
||||
Oid: githubv4.GitObjectID(branchID),
|
||||
}
|
||||
if repoID != "" {
|
||||
repo := githubv4.ID(repoID)
|
||||
input.RepositoryID = &repo
|
||||
}
|
||||
if branchName != "" {
|
||||
name := githubv4.String(branchName)
|
||||
input.Name = &name
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": input,
|
||||
}
|
||||
|
||||
err := client.Mutate(host, "CreateLinkedBranch", &mutation, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
ref := LinkedBranch{
|
||||
ID: result.CreateLinkedBranch.LinkedBranch.ID,
|
||||
BranchName: result.CreateLinkedBranch.LinkedBranch.Ref.Name,
|
||||
}
|
||||
return &ref, nil
|
||||
|
||||
return mutation.CreateLinkedBranch.LinkedBranch.Ref.Name, nil
|
||||
}
|
||||
|
||||
func ListLinkedBranches(client *Client, repo ghrepo.Interface, issueNumber int) ([]LinkedBranch, error) {
|
||||
query := `
|
||||
query BranchIssueReferenceListLinkedBranches($repositoryName: String!, $repositoryOwner: String!, $issueNumber: Int!) {
|
||||
repository(name: $repositoryName, owner: $repositoryOwner) {
|
||||
issue(number: $issueNumber) {
|
||||
linkedBranches(first: 30) {
|
||||
edges {
|
||||
node {
|
||||
ref {
|
||||
name
|
||||
repository {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
variables := map[string]interface{}{
|
||||
"repositoryName": repo.RepoName(),
|
||||
"repositoryOwner": repo.RepoOwner(),
|
||||
"issueNumber": issueNumber,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Issue struct {
|
||||
LinkedBranches struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Ref struct {
|
||||
Name string
|
||||
Repository struct {
|
||||
NameWithOwner string
|
||||
Url string
|
||||
}
|
||||
Nodes []struct {
|
||||
Ref struct {
|
||||
Name string
|
||||
Repository struct {
|
||||
Url string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &result)
|
||||
var branchNames []LinkedBranch
|
||||
if err != nil {
|
||||
return branchNames, err
|
||||
} `graphql:"linkedBranches(first: 30)"`
|
||||
} `graphql:"issue(number: $number)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
for _, edge := range result.Repository.Issue.LinkedBranches.Edges {
|
||||
branch := LinkedBranch{
|
||||
BranchName: edge.Node.Ref.Name,
|
||||
RepoUrl: edge.Node.Ref.Repository.Url,
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"number": githubv4.Int(issueNumber),
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
}
|
||||
|
||||
if err := client.Query(repo.RepoHost(), "ListLinkedBranches", &query, variables); err != nil {
|
||||
return []LinkedBranch{}, err
|
||||
}
|
||||
|
||||
var branchNames []LinkedBranch
|
||||
|
||||
for _, node := range query.Repository.Issue.LinkedBranches.Nodes {
|
||||
branch := LinkedBranch{
|
||||
BranchName: node.Ref.Name,
|
||||
URL: fmt.Sprintf("%s/tree/%s", node.Ref.Repository.Url, node.Ref.Name),
|
||||
}
|
||||
branchNames = append(branchNames, branch)
|
||||
}
|
||||
|
||||
return branchNames, nil
|
||||
|
||||
}
|
||||
|
||||
// introspects the schema to see if we expose the LinkedBranch type
|
||||
func CheckLinkedBranchFeature(client *Client, host string) (err error) {
|
||||
var featureDetection struct {
|
||||
func CheckLinkedBranchFeature(client *Client, host string) error {
|
||||
var query struct {
|
||||
Name struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
|
|
@ -160,44 +98,21 @@ func CheckLinkedBranchFeature(client *Client, host string) (err error) {
|
|||
} `graphql:"LinkedBranch: __type(name: \"LinkedBranch\")"`
|
||||
}
|
||||
|
||||
err = client.Query(host, "LinkedBranch_fields", &featureDetection, nil)
|
||||
|
||||
if err != nil {
|
||||
if err := client.Query(host, "LinkedBranchFeature", &query, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(featureDetection.Name.Fields) == 0 {
|
||||
if len(query.Name.Fields) == 0 {
|
||||
return fmt.Errorf("the `gh issue develop` command is not currently available")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This fetches the oids for the repo's default branch (`main`, etc) and the name the user might have provided in one shot.
|
||||
func FindBaseOid(client *Client, repo *Repository, ref string) (string, string, error) {
|
||||
query := `
|
||||
query BranchIssueReferenceFindBaseOid($repositoryName: String!, $repositoryOwner: String!, $ref: String!) {
|
||||
repository(name: $repositoryName, owner: $repositoryOwner) {
|
||||
defaultBranchRef {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
ref(qualifiedName: $ref) {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"repositoryName": repo.Name,
|
||||
"repositoryOwner": repo.RepoOwner(),
|
||||
"ref": ref,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
func FindRepoBranchID(client *Client, repo ghrepo.Interface, ref string) (string, string, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Id string
|
||||
DefaultBranchRef struct {
|
||||
Target struct {
|
||||
Oid string
|
||||
|
|
@ -207,13 +122,24 @@ func FindBaseOid(client *Client, repo *Repository, ref string) (string, string,
|
|||
Target struct {
|
||||
Oid string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
} `graphql:"ref(qualifiedName: $ref)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &result)
|
||||
if err != nil {
|
||||
variables := map[string]interface{}{
|
||||
"ref": githubv4.String(ref),
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
}
|
||||
|
||||
if err := client.Query(repo.RepoHost(), "FindRepoBranchID", &query, variables); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return result.Repository.Ref.Target.Oid, result.Repository.DefaultBranchRef.Target.Oid, nil
|
||||
|
||||
branchID := query.Repository.Ref.Target.Oid
|
||||
if branchID == "" {
|
||||
branchID = query.Repository.DefaultBranchRef.Target.Oid
|
||||
}
|
||||
|
||||
return query.Repository.Id, branchID, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ type PullRequest struct {
|
|||
ClosedAt *time.Time
|
||||
MergedAt *time.Time
|
||||
|
||||
AutoMergeRequest *AutoMergeRequest
|
||||
|
||||
MergeCommit *Commit
|
||||
PotentialMergeCommit *Commit
|
||||
|
||||
|
|
@ -95,7 +97,84 @@ type CommitStatusCheckRollup struct {
|
|||
Contexts CheckContexts
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkrunstate
|
||||
type CheckRunState string
|
||||
|
||||
const (
|
||||
CheckRunStateActionRequired CheckRunState = "ACTION_REQUIRED"
|
||||
CheckRunStateCancelled CheckRunState = "CANCELLED"
|
||||
CheckRunStateCompleted CheckRunState = "COMPLETED"
|
||||
CheckRunStateFailure CheckRunState = "FAILURE"
|
||||
CheckRunStateInProgress CheckRunState = "IN_PROGRESS"
|
||||
CheckRunStateNeutral CheckRunState = "NEUTRAL"
|
||||
CheckRunStatePending CheckRunState = "PENDING"
|
||||
CheckRunStateQueued CheckRunState = "QUEUED"
|
||||
CheckRunStateSkipped CheckRunState = "SKIPPED"
|
||||
CheckRunStateStale CheckRunState = "STALE"
|
||||
CheckRunStateStartupFailure CheckRunState = "STARTUP_FAILURE"
|
||||
CheckRunStateSuccess CheckRunState = "SUCCESS"
|
||||
CheckRunStateTimedOut CheckRunState = "TIMED_OUT"
|
||||
CheckRunStateWaiting CheckRunState = "WAITING"
|
||||
)
|
||||
|
||||
type CheckRunCountByState struct {
|
||||
State CheckRunState
|
||||
Count int
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#statusstate
|
||||
type StatusState string
|
||||
|
||||
const (
|
||||
StatusStateError StatusState = "ERROR"
|
||||
StatusStateExpected StatusState = "EXPECTED"
|
||||
StatusStateFailure StatusState = "FAILURE"
|
||||
StatusStatePending StatusState = "PENDING"
|
||||
StatusStateSuccess StatusState = "SUCCESS"
|
||||
)
|
||||
|
||||
type StatusContextCountByState struct {
|
||||
State StatusState
|
||||
Count int
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkstatusstate
|
||||
type CheckStatusState string
|
||||
|
||||
const (
|
||||
CheckStatusStateCompleted CheckStatusState = "COMPLETED"
|
||||
CheckStatusStateInProgress CheckStatusState = "IN_PROGRESS"
|
||||
CheckStatusStatePending CheckStatusState = "PENDING"
|
||||
CheckStatusStateQueued CheckStatusState = "QUEUED"
|
||||
CheckStatusStateRequested CheckStatusState = "REQUESTED"
|
||||
CheckStatusStateWaiting CheckStatusState = "WAITING"
|
||||
)
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkconclusionstate
|
||||
type CheckConclusionState string
|
||||
|
||||
const (
|
||||
CheckConclusionStateActionRequired CheckConclusionState = "ACTION_REQUIRED"
|
||||
CheckConclusionStateCancelled CheckConclusionState = "CANCELLED"
|
||||
CheckConclusionStateFailure CheckConclusionState = "FAILURE"
|
||||
CheckConclusionStateNeutral CheckConclusionState = "NEUTRAL"
|
||||
CheckConclusionStateSkipped CheckConclusionState = "SKIPPED"
|
||||
CheckConclusionStateStale CheckConclusionState = "STALE"
|
||||
CheckConclusionStateStartupFailure CheckConclusionState = "STARTUP_FAILURE"
|
||||
CheckConclusionStateSuccess CheckConclusionState = "SUCCESS"
|
||||
CheckConclusionStateTimedOut CheckConclusionState = "TIMED_OUT"
|
||||
)
|
||||
|
||||
type CheckContexts struct {
|
||||
// These fields are available on newer versions of the GraphQL API
|
||||
// to support summary counts by state
|
||||
CheckRunCount int
|
||||
CheckRunCountsByState []CheckRunCountByState
|
||||
StatusContextCount int
|
||||
StatusContextCountsByState []StatusContextCountByState
|
||||
|
||||
// These are available on older versions and provide more details
|
||||
// required for checks
|
||||
Nodes []CheckContext
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
|
|
@ -104,31 +183,38 @@ type CheckContexts struct {
|
|||
}
|
||||
|
||||
type CheckContext struct {
|
||||
TypeName string `json:"__typename"`
|
||||
Name string `json:"name"`
|
||||
IsRequired bool `json:"isRequired"`
|
||||
CheckSuite struct {
|
||||
WorkflowRun struct {
|
||||
Workflow struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"workflow"`
|
||||
} `json:"workflowRun"`
|
||||
} `json:"checkSuite"`
|
||||
TypeName string `json:"__typename"`
|
||||
Name string `json:"name"`
|
||||
IsRequired bool `json:"isRequired"`
|
||||
CheckSuite CheckSuite `json:"checkSuite"`
|
||||
// QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED
|
||||
Status string `json:"status"`
|
||||
// ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE
|
||||
Conclusion string `json:"conclusion"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
Conclusion CheckConclusionState `json:"conclusion"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
|
||||
/* StatusContext fields */
|
||||
|
||||
Context string `json:"context"`
|
||||
Context string `json:"context"`
|
||||
Description string `json:"description"`
|
||||
// EXPECTED ERROR FAILURE PENDING SUCCESS
|
||||
State string `json:"state"`
|
||||
TargetURL string `json:"targetUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
State StatusState `json:"state"`
|
||||
TargetURL string `json:"targetUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CheckSuite struct {
|
||||
WorkflowRun WorkflowRun `json:"workflowRun"`
|
||||
}
|
||||
|
||||
type WorkflowRun struct {
|
||||
Event string `json:"event"`
|
||||
Workflow Workflow `json:"workflow"`
|
||||
}
|
||||
|
||||
type Workflow struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type PRRepository struct {
|
||||
|
|
@ -136,6 +222,16 @@ type PRRepository struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AutoMergeRequest struct {
|
||||
AuthorEmail *string `json:"authorEmail"`
|
||||
CommitBody *string `json:"commitBody"`
|
||||
CommitHeadline *string `json:"commitHeadline"`
|
||||
// MERGE, REBASE, SQUASH
|
||||
MergeMethod string `json:"mergeMethod"`
|
||||
EnabledAt time.Time `json:"enabledAt"`
|
||||
EnabledBy Author `json:"enabledBy"`
|
||||
}
|
||||
|
||||
// Commit loads just the commit SHA and nothing else
|
||||
type Commit struct {
|
||||
OID string `json:"oid"`
|
||||
|
|
@ -249,33 +345,136 @@ type PullRequestChecksStatus struct {
|
|||
Total int
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
||||
func (pr *PullRequest) ChecksStatus() PullRequestChecksStatus {
|
||||
var summary PullRequestChecksStatus
|
||||
|
||||
if len(pr.StatusCheckRollup.Nodes) == 0 {
|
||||
return
|
||||
return summary
|
||||
}
|
||||
commit := pr.StatusCheckRollup.Nodes[0].Commit
|
||||
for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
|
||||
state := c.State // StatusContext
|
||||
if state == "" {
|
||||
// CheckRun
|
||||
if c.Status == "COMPLETED" {
|
||||
state = c.Conclusion
|
||||
} else {
|
||||
state = c.Status
|
||||
|
||||
contexts := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts
|
||||
|
||||
// If this commit has counts by state then we can summarise check status from those
|
||||
if len(contexts.CheckRunCountsByState) != 0 && len(contexts.StatusContextCountsByState) != 0 {
|
||||
summary.Total = contexts.CheckRunCount + contexts.StatusContextCount
|
||||
for _, countByState := range contexts.CheckRunCountsByState {
|
||||
switch parseCheckStatusFromCheckRunState(countByState.State) {
|
||||
case passing:
|
||||
summary.Passing += countByState.Count
|
||||
case failing:
|
||||
summary.Failing += countByState.Count
|
||||
default:
|
||||
summary.Pending += countByState.Count
|
||||
}
|
||||
}
|
||||
switch state {
|
||||
case "SUCCESS", "NEUTRAL", "SKIPPED":
|
||||
summary.Passing++
|
||||
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
|
||||
summary.Failing++
|
||||
default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
|
||||
summary.Pending++
|
||||
|
||||
for _, countByState := range contexts.StatusContextCountsByState {
|
||||
switch parseCheckStatusFromStatusState(countByState.State) {
|
||||
case passing:
|
||||
summary.Passing += countByState.Count
|
||||
case failing:
|
||||
summary.Failing += countByState.Count
|
||||
default:
|
||||
summary.Pending += countByState.Count
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// If we don't have the counts by state, then we'll need to summarise by looking at the more detailed contexts
|
||||
for _, c := range contexts.Nodes {
|
||||
// Nodes are a discriminated union of CheckRun or StatusContext and we can match on
|
||||
// the TypeName to narrow the type.
|
||||
if c.TypeName == "CheckRun" {
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkstatusstate
|
||||
// If the status is completed then we can check the conclusion field
|
||||
if c.Status == "COMPLETED" {
|
||||
switch parseCheckStatusFromCheckConclusionState(c.Conclusion) {
|
||||
case passing:
|
||||
summary.Passing++
|
||||
case failing:
|
||||
summary.Failing++
|
||||
default:
|
||||
summary.Pending++
|
||||
}
|
||||
// otherwise we're in some form of pending state:
|
||||
// "COMPLETED", "IN_PROGRESS", "PENDING", "QUEUED", "REQUESTED", "WAITING" or otherwise unknown
|
||||
} else {
|
||||
summary.Pending++
|
||||
}
|
||||
|
||||
} else { // c.TypeName == StatusContext
|
||||
switch parseCheckStatusFromStatusState(c.State) {
|
||||
case passing:
|
||||
summary.Passing++
|
||||
case failing:
|
||||
summary.Failing++
|
||||
default:
|
||||
summary.Pending++
|
||||
}
|
||||
}
|
||||
summary.Total++
|
||||
}
|
||||
|
||||
return
|
||||
return summary
|
||||
}
|
||||
|
||||
type checkStatus int
|
||||
|
||||
const (
|
||||
passing checkStatus = iota
|
||||
failing
|
||||
pending
|
||||
)
|
||||
|
||||
func parseCheckStatusFromStatusState(state StatusState) checkStatus {
|
||||
switch state {
|
||||
case StatusStateSuccess:
|
||||
return passing
|
||||
case StatusStateFailure, StatusStateError:
|
||||
return failing
|
||||
case StatusStateExpected, StatusStatePending:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheckStatusFromCheckRunState(state CheckRunState) checkStatus {
|
||||
switch state {
|
||||
case CheckRunStateNeutral, CheckRunStateSkipped, CheckRunStateSuccess:
|
||||
return passing
|
||||
case CheckRunStateActionRequired, CheckRunStateCancelled, CheckRunStateFailure, CheckRunStateTimedOut:
|
||||
return failing
|
||||
case CheckRunStateCompleted, CheckRunStateInProgress, CheckRunStatePending, CheckRunStateQueued,
|
||||
CheckRunStateStale, CheckRunStateStartupFailure, CheckRunStateWaiting:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheckStatusFromCheckConclusionState(state CheckConclusionState) checkStatus {
|
||||
switch state {
|
||||
case CheckConclusionStateNeutral, CheckConclusionStateSkipped, CheckConclusionStateSuccess:
|
||||
return passing
|
||||
case CheckConclusionStateActionRequired, CheckConclusionStateCancelled, CheckConclusionStateFailure, CheckConclusionStateTimedOut:
|
||||
return failing
|
||||
case CheckConclusionStateStale, CheckConclusionStateStartupFailure:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PullRequest) DisplayableReviews() PullRequestReviews {
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ func CurrentUserProjectsV2(client *Client, hostname string) ([]ProjectV2, error)
|
|||
// When querying ProjectsV2 fields we generally dont want to show the user
|
||||
// scope errors and field does not exist errors. ProjectsV2IgnorableError
|
||||
// checks against known error strings to see if an error can be safely ignored.
|
||||
// Due to the fact that the GQLClient can return multiple types of errors
|
||||
// Due to the fact that the GraphQLClient can return multiple types of errors
|
||||
// this uses brittle string comparison to check against the known error strings.
|
||||
func ProjectsV2IgnorableError(err error) bool {
|
||||
msg := err.Error()
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ import (
|
|||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/go-gh/pkg/api"
|
||||
ghAPI "github.com/cli/go-gh/pkg/api"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
|
|
@ -264,8 +263,8 @@ func FetchRepository(client *Client, repo ghrepo.Interface, fields []string) (*R
|
|||
// guaranteed to happen when an authentication token with insufficient permissions is being used.
|
||||
if result.Repository == nil {
|
||||
return nil, GraphQLError{
|
||||
GQLError: ghAPI.GQLError{
|
||||
Errors: []ghAPI.GQLErrorItem{{
|
||||
GraphQLError: &ghAPI.GraphQLError{
|
||||
Errors: []ghAPI.GraphQLErrorItem{{
|
||||
Type: "NOT_FOUND",
|
||||
Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()),
|
||||
}},
|
||||
|
|
@ -317,8 +316,8 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
|||
// guaranteed to happen when an authentication token with insufficient permissions is being used.
|
||||
if result.Repository == nil {
|
||||
return nil, GraphQLError{
|
||||
GQLError: ghAPI.GQLError{
|
||||
Errors: []ghAPI.GQLErrorItem{{
|
||||
GraphQLError: &ghAPI.GraphQLError{
|
||||
Errors: []ghAPI.GraphQLErrorItem{{
|
||||
Type: "NOT_FOUND",
|
||||
Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()),
|
||||
}},
|
||||
|
|
@ -1372,7 +1371,6 @@ func GetRepoIDs(client *Client, host string, repositories []ghrepo.Interface) ([
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// RenameRepo renames the repository on GitHub and returns the renamed repository
|
||||
func RepoExists(client *Client, repo ghrepo.Interface) (bool, error) {
|
||||
path := fmt.Sprintf("%srepos/%s/%s", ghinstance.RESTPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName())
|
||||
|
||||
|
|
@ -1387,6 +1385,6 @@ func RepoExists(client *Client, repo ghrepo.Interface) (bool, error) {
|
|||
case 404:
|
||||
return false, nil
|
||||
default:
|
||||
return false, api.HandleHTTPError(resp)
|
||||
return false, ghAPI.HandleHTTPError(resp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,42 @@ var prCommits = shortenQuery(`
|
|||
}
|
||||
`)
|
||||
|
||||
func StatusCheckRollupGraphQL(after string) string {
|
||||
var autoMergeRequest = shortenQuery(`
|
||||
autoMergeRequest {
|
||||
authorEmail,
|
||||
commitBody,
|
||||
commitHeadline,
|
||||
mergeMethod,
|
||||
enabledAt,
|
||||
enabledBy{login,...on User{id,name}}
|
||||
}
|
||||
`)
|
||||
|
||||
func StatusCheckRollupGraphQLWithCountByState() string {
|
||||
return shortenQuery(`
|
||||
statusCheckRollup: commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
contexts {
|
||||
checkRunCount,
|
||||
checkRunCountsByState {
|
||||
state,
|
||||
count
|
||||
},
|
||||
statusContextCount,
|
||||
statusContextCountsByState {
|
||||
state,
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func StatusCheckRollupGraphQLWithoutCountByState(after string) string {
|
||||
var afterClause string
|
||||
if after != "" {
|
||||
afterClause = ",after:" + after
|
||||
|
|
@ -149,7 +184,8 @@ func StatusCheckRollupGraphQL(after string) string {
|
|||
context,
|
||||
state,
|
||||
targetUrl,
|
||||
createdAt
|
||||
createdAt,
|
||||
description
|
||||
},
|
||||
...on CheckRun {
|
||||
name,
|
||||
|
|
@ -187,11 +223,12 @@ func RequiredStatusCheckRollupGraphQL(prID, after string) string {
|
|||
state,
|
||||
targetUrl,
|
||||
createdAt,
|
||||
description,
|
||||
isRequired(pullRequestId: %[2]s)
|
||||
},
|
||||
...on CheckRun {
|
||||
name,
|
||||
checkSuite{workflowRun{workflow{name}}},
|
||||
checkSuite{workflowRun{event,workflow{name}}},
|
||||
status,
|
||||
conclusion,
|
||||
startedAt,
|
||||
|
|
@ -231,6 +268,7 @@ var IssueFields = []string{
|
|||
|
||||
var PullRequestFields = append(IssueFields,
|
||||
"additions",
|
||||
"autoMergeRequest",
|
||||
"baseRefName",
|
||||
"changedFiles",
|
||||
"commits",
|
||||
|
|
@ -285,6 +323,8 @@ func IssueGraphQL(fields []string) string {
|
|||
q = append(q, `mergeCommit{oid}`)
|
||||
case "potentialMergeCommit":
|
||||
q = append(q, `potentialMergeCommit{oid}`)
|
||||
case "autoMergeRequest":
|
||||
q = append(q, autoMergeRequest)
|
||||
case "comments":
|
||||
q = append(q, issueComments)
|
||||
case "lastComment": // pseudo-field
|
||||
|
|
@ -306,7 +346,9 @@ func IssueGraphQL(fields []string) string {
|
|||
case "requiresStrictStatusChecks": // pseudo-field
|
||||
q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`)
|
||||
case "statusCheckRollup":
|
||||
q = append(q, StatusCheckRollupGraphQL(""))
|
||||
q = append(q, StatusCheckRollupGraphQLWithoutCountByState(""))
|
||||
case "statusCheckRollupWithCountByState": // pseudo-field
|
||||
q = append(q, StatusCheckRollupGraphQLWithCountByState())
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
|
|
@ -372,6 +414,7 @@ var RepositoryFields = []string{
|
|||
"isInOrganization",
|
||||
"isMirror",
|
||||
"isPrivate",
|
||||
"visibility",
|
||||
"isTemplate",
|
||||
"isUserConfigurationRepository",
|
||||
"licenseInfo",
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ type sanitizer struct {
|
|||
addEscape bool
|
||||
}
|
||||
|
||||
// Transform uses a sliding window alogorithm to detect C0 and C1
|
||||
// Transform uses a sliding window algorithm to detect C0 and C1
|
||||
// ASCII control sequences as they are read and replaces them
|
||||
// with equivelent inert characters. Characters that are not part
|
||||
// with equivalent inert characters. Characters that are not part
|
||||
// of a control sequence are not modified.
|
||||
func (t *sanitizer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
lSrc := len(src)
|
||||
|
|
|
|||
|
|
@ -30,9 +30,5 @@
|
|||
<WixExtension Include="WixUIExtension"/>
|
||||
<WixExtension Include="WixUtilExtension"/>
|
||||
</ItemGroup>
|
||||
<Target Name="SetStepOutput" AfterTargets="Build" Condition="'$(GITHUB_ACTIONS)' != ''">
|
||||
<!-- Make sure the correct target path is always set as the step output -->
|
||||
<Message Importance="high" Text="::set-output name=msi::$(TargetPath)"/>
|
||||
</Target>
|
||||
<Import Project="$(WixTargetsPath)"/>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/docs"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
|
@ -41,9 +45,13 @@ func run(args []string) error {
|
|||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
rootCmd := root.NewCmdRoot(&cmdutil.Factory{
|
||||
rootCmd, _ := root.NewCmdRoot(&cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Browser: &browser{},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewFromString(""), nil
|
||||
},
|
||||
ExtensionManager: &em{},
|
||||
}, "", "")
|
||||
rootCmd.InitDefaultHelpCmd()
|
||||
|
||||
|
|
@ -79,8 +87,42 @@ func linkHandler(name string) string {
|
|||
return fmt.Sprintf("./%s", strings.TrimSuffix(name, ".md"))
|
||||
}
|
||||
|
||||
// Implements browser.Browser interface.
|
||||
type browser struct{}
|
||||
|
||||
func (b *browser) Browse(url string) error {
|
||||
func (b *browser) Browse(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements extensions.ExtensionManager interface.
|
||||
type em struct{}
|
||||
|
||||
func (e *em) List() []extensions.Extension {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Install(_ ghrepo.Interface, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) InstallLocal(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Upgrade(_ string, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Remove(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Dispatch(_ []string, _ io.Reader, _, _ io.Writer) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *em) Create(_ string, _ extensions.ExtTemplateType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) EnableDryRunMode() {}
|
||||
|
|
|
|||
159
cmd/gh/main.go
159
cmd/gh/main.go
|
|
@ -14,16 +14,10 @@ import (
|
|||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/expand"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -80,9 +74,6 @@ func mainRun() exitCode {
|
|||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
if cmdFactory.IOStreams.ColorSupport256() {
|
||||
return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
|
||||
}
|
||||
return ansi.ColorCode("default")
|
||||
default:
|
||||
return ansi.ColorCode(style)
|
||||
|
|
@ -96,11 +87,9 @@ func mainRun() exitCode {
|
|||
cobra.MousetrapHelpText = ""
|
||||
}
|
||||
|
||||
rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
|
||||
cfg, err := cmdFactory.Config()
|
||||
rootCmd, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
|
|
@ -109,110 +98,10 @@ func mainRun() exitCode {
|
|||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions
|
||||
if len(expandedArgs) == 2 && expandedArgs[0] == "help" && !hasCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = []string{expandedArgs[1], "--help"}
|
||||
}
|
||||
|
||||
if !hasCommand(rootCmd, expandedArgs) {
|
||||
originalArgs := expandedArgs
|
||||
isShell := false
|
||||
|
||||
argsForExpansion := append([]string{"gh"}, expandedArgs...)
|
||||
expandedArgs, isShell, err = expand.ExpandAlias(cfg, argsForExpansion, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
if hasDebug {
|
||||
fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs)
|
||||
}
|
||||
|
||||
if isShell {
|
||||
exe, err := safeexec.LookPath(expandedArgs[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
externalCmd := exec.Command(exe, expandedArgs[1:]...)
|
||||
externalCmd.Stderr = os.Stderr
|
||||
externalCmd.Stdout = os.Stdout
|
||||
externalCmd.Stdin = os.Stdin
|
||||
preparedCmd := run.PrepareCmd(externalCmd)
|
||||
|
||||
err = preparedCmd.Run()
|
||||
if err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
return exitOK
|
||||
} else if len(expandedArgs) > 0 && !hasCommand(rootCmd, expandedArgs) {
|
||||
extensionManager := cmdFactory.ExtensionManager
|
||||
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run extension: %s\n", err)
|
||||
return exitError
|
||||
} else if found {
|
||||
return exitOK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// provide completions for aliases and extensions
|
||||
rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var results []string
|
||||
aliases := cfg.Aliases()
|
||||
for aliasName, aliasValue := range aliases.All() {
|
||||
if strings.HasPrefix(aliasName, toComplete) {
|
||||
var s string
|
||||
if strings.HasPrefix(aliasValue, "!") {
|
||||
s = fmt.Sprintf("%s\tShell alias", aliasName)
|
||||
} else {
|
||||
aliasValue = text.Truncate(80, aliasValue)
|
||||
s = fmt.Sprintf("%s\tAlias for %s", aliasName, aliasValue)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
for _, ext := range cmdFactory.ExtensionManager.List() {
|
||||
if strings.HasPrefix(ext.Name(), toComplete) {
|
||||
var s string
|
||||
if ext.IsLocal() {
|
||||
s = fmt.Sprintf("%s\tLocal extension gh-%s", ext.Name(), ext.Name())
|
||||
} else {
|
||||
path := ext.URL()
|
||||
if u, err := git.ParseURL(ext.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
path = ghrepo.FullName(r)
|
||||
}
|
||||
}
|
||||
s = fmt.Sprintf("%s\tExtension %s", ext.Name(), path)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
return results, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
authError := errors.New("authError")
|
||||
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// require that the user is authenticated before running most commands
|
||||
if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
|
||||
fmt.Fprint(stderr, authHelp())
|
||||
return authError
|
||||
}
|
||||
|
||||
return nil
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
|
@ -220,6 +109,8 @@ func mainRun() exitCode {
|
|||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var extError *root.ExternalCommandExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
|
|
@ -228,7 +119,7 @@ func mainRun() exitCode {
|
|||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.Is(err, authError) {
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
|
|
@ -239,6 +130,9 @@ func mainRun() exitCode {
|
|||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &extError) {
|
||||
// pass on exit codes from extensions and shell aliases
|
||||
return exitCode(extError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
|
@ -287,10 +181,10 @@ func mainRun() exitCode {
|
|||
return exitOK
|
||||
}
|
||||
|
||||
// hasCommand returns true if args resolve to a built-in command
|
||||
func hasCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Traverse(args)
|
||||
return err == nil && c != rootCmd
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
||||
|
|
@ -315,27 +209,6 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func authHelp() string {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
`)
|
||||
}
|
||||
|
||||
if os.Getenv("CI") != "" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.
|
||||
`)
|
||||
}
|
||||
|
||||
return heredoc.Doc(`
|
||||
To get started with GitHub CLI, please run: gh auth login
|
||||
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
|
||||
`)
|
||||
}
|
||||
|
||||
func shouldCheckForUpdate() bool {
|
||||
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,36 +1,40 @@
|
|||
# Releasing
|
||||
|
||||
Our build system automatically compiles and attaches cross-platform binaries to any git tag named `vX.Y.Z`. The changelog is [generated from git commit log](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes).
|
||||
To initiate a new production deployment:
|
||||
```sh
|
||||
script/release vX.Y.Z
|
||||
```
|
||||
See `script/release --help` for more information.
|
||||
|
||||
Users who run official builds of `gh` on their machines will get notified about the new version within a 24 hour period.
|
||||
> **Note:**
|
||||
> Every production release will request an approval by the select few people before it can proceed.
|
||||
|
||||
To test out the build system, publish a prerelease tag with a name such as `vX.Y.Z-pre.0` or `vX.Y.Z-rc.1`. Note that such a release will still be public, but it will be marked as a "prerelease", meaning that it will never show up as a "latest" release nor trigger upgrade notifications for users.
|
||||
What this does is:
|
||||
- Builds Linux binaries on Ubuntu;
|
||||
- Builds and signs Windows binaries on Windows;
|
||||
- Builds, signs, and notarizes macOS binaries on macOS;
|
||||
- Uploads all release artifacts to a new GitHub Release;
|
||||
- A new git tag `vX.Y.Z` is created in the remote repository;
|
||||
- The changelog is [generated from the list of merged pull requests](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes);
|
||||
- Updates cli.github.com with the contents of the new release;
|
||||
- Updates our Homebrew formula in the homebrew-core repo.
|
||||
|
||||
To test out the build system while avoiding creating an actual release:
|
||||
```sh
|
||||
script/release --staging vX.Y.Z --branch patch-1 -p macos
|
||||
```
|
||||
The build artifacts will be available via `gh run download <RUN> -n macos`.
|
||||
|
||||
## General guidelines
|
||||
|
||||
* Features to be released should be reviewed and approved at least one day prior to the release.
|
||||
* Feature releases should bump up the minor version number.
|
||||
* Breaking releases should bump up the major version number. These should generally be rare.
|
||||
|
||||
## Tagging a new release
|
||||
|
||||
1. `git tag v1.2.3 && git push origin v1.2.3`
|
||||
2. Wait several minutes for builds to run: <https://github.com/cli/cli/actions>
|
||||
3. Verify release is displayed and has correct assets: <https://github.com/cli/cli/releases>
|
||||
4. Scan generated release notes and optionally add a human touch by grouping items under topic sections
|
||||
5. Verify the marketing site was updated: <https://cli.github.com>
|
||||
6. (Optional) Delete any pre-releases related to this release
|
||||
|
||||
A successful build will result in changes across several repositories:
|
||||
* <https://github.com/github/cli.github.com>
|
||||
* <https://github.com/Homebrew/homebrew-core/pulls>
|
||||
* <https://github.com/cli/scoop-gh>
|
||||
|
||||
If the build fails, there is not a clean way to re-run it. The easiest way would be to start over by deleting the partial release on GitHub and re-publishing the tag. Note that this might be disruptive to users or tooling that were already notified about an upgrade. If a functional release and its binaries are already out there, it might be better to try to manually fix up only the specific workflow tasks that failed. Use your best judgement depending on the failure type.
|
||||
|
||||
## Release locally for debugging
|
||||
## Test the build system locally
|
||||
|
||||
A local release can be created for testing without creating anything official on the release page.
|
||||
|
||||
1. Make sure GoReleaser is installed: `brew install goreleaser`
|
||||
2. `goreleaser --skip-validate --skip-publish --rm-dist`
|
||||
2. `script/release --local`
|
||||
3. Find the built products under `dist/`.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ To be considered triaged, `enhancement` issues require at least one of the follo
|
|||
- `needs-investigation`: work that requires a mystery be solved by the core team before it can move forward
|
||||
- `needs-user-input`: we need more information from our users before the task can move forward
|
||||
|
||||
To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`
|
||||
To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`, which are defined as follows:
|
||||
- `p1`: Affects a large population and inhibits work
|
||||
- `p2`: Affects more than a few users but doesn't prevent core functions
|
||||
- `p3`: Affects a small number of users or is largely cosmetic
|
||||
|
||||
## Expectations for community pull requests
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,19 @@ type Client struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Client) Copy() *Client {
|
||||
return &Client{
|
||||
GhPath: c.GhPath,
|
||||
RepoDir: c.RepoDir,
|
||||
GitPath: c.GitPath,
|
||||
Stderr: c.Stderr,
|
||||
Stdin: c.Stdin,
|
||||
Stdout: c.Stdout,
|
||||
|
||||
commandContext: c.commandContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) {
|
||||
if c.RepoDir != "" {
|
||||
args = append([]string{"-C", c.RepoDir}, args...)
|
||||
|
|
@ -408,6 +421,44 @@ func (c *Client) revParse(ctx context.Context, args ...string) ([]byte, error) {
|
|||
return cmd.Output()
|
||||
}
|
||||
|
||||
func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
|
||||
_, err := c.GitDir(ctx)
|
||||
if err != nil {
|
||||
var execError errWithExitCode
|
||||
if errors.As(err, &execError) && execError.ExitCode() == 128 {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
|
||||
args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SetRemoteBranches(ctx context.Context, remote string, refspec string) error {
|
||||
args := []string{"remote", "set-branches", remote, refspec}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Below are commands that make network calls and need authentication credentials supplied from gh.
|
||||
|
||||
func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error {
|
||||
|
|
@ -513,31 +564,6 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra
|
|||
return remote, nil
|
||||
}
|
||||
|
||||
func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
|
||||
_, err := c.GitDir(ctx)
|
||||
if err != nil {
|
||||
var execError errWithExitCode
|
||||
if errors.As(err, &execError) && execError.ExitCode() == 128 {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
|
||||
args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveGitPath() (string, error) {
|
||||
path, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ func TestClientCurrentBranch(t *testing.T) {
|
|||
wantBranch: "branch\u00A0with\u00A0non\u00A0breaking\u00A0space",
|
||||
},
|
||||
{
|
||||
name: "detatched head",
|
||||
name: "detached head",
|
||||
cmdExitStatus: 1,
|
||||
wantCmdArgs: `path/to/git symbolic-ref --quiet HEAD`,
|
||||
wantErrorMsg: "failed to run git: not on any branch",
|
||||
|
|
@ -339,7 +339,7 @@ func TestClientShowRefs(t *testing.T) {
|
|||
wantErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "show refs with one vaid ref and one invalid ref",
|
||||
name: "show refs with one valid ref and one invalid ref",
|
||||
cmdExitStatus: 128,
|
||||
cmdStdout: "9ea76237a557015e73446d33268569a114c0649c refs/heads/valid",
|
||||
cmdStderr: "fatal: 'refs/heads/invalid' - not a valid ref",
|
||||
|
|
@ -834,6 +834,84 @@ func TestClientPathFromRoot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientUnsetRemoteResolution(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
wantCmdArgs string
|
||||
wantErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "unset remote resolution",
|
||||
wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`,
|
||||
},
|
||||
{
|
||||
name: "git error",
|
||||
cmdExitStatus: 1,
|
||||
cmdStderr: "git error message",
|
||||
wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`,
|
||||
wantErrorMsg: "failed to run git: git error message",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
err := client.UnsetRemoteResolution(context.Background(), "origin")
|
||||
assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
if tt.wantErrorMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErrorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSetRemoteBranches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
wantCmdArgs string
|
||||
wantErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "set remote branches",
|
||||
wantCmdArgs: `path/to/git remote set-branches origin trunk`,
|
||||
},
|
||||
{
|
||||
name: "git error",
|
||||
cmdExitStatus: 1,
|
||||
cmdStderr: "git error message",
|
||||
wantCmdArgs: `path/to/git remote set-branches origin trunk`,
|
||||
wantErrorMsg: "failed to run git: git error message",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
err := client.SetRemoteBranches(context.Background(), "origin", "trunk")
|
||||
assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
if tt.wantErrorMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErrorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientFetch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
50
go.mod
50
go.mod
|
|
@ -3,13 +3,13 @@ module github.com/cli/cli/v2
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.0
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/cli/go-gh v1.2.1
|
||||
github.com/cli/go-gh/v2 v2.0.1
|
||||
github.com/cli/oauth v1.0.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2
|
||||
|
|
@ -21,27 +21,28 @@ require (
|
|||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/henvic/httpretty v0.0.6
|
||||
github.com/henvic/httpretty v0.1.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/mattn/go-isatty v0.0.19
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.5
|
||||
github.com/zalando/go-keyring v0.2.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/zalando/go-keyring v0.2.3
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.6.0
|
||||
golang.org/x/text v0.8.0
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
golang.org/x/term v0.7.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
|
@ -49,9 +50,9 @@ require (
|
|||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cli/browser v1.1.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.2 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/cli/browser v1.2.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.3 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
|
|
@ -59,10 +60,12 @@ require (
|
|||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/itchyny/gojq v0.12.8 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.3 // indirect
|
||||
github.com/itchyny/gojq v0.12.13 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.20 // indirect
|
||||
|
|
@ -70,20 +73,17 @@ require (
|
|||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/yuin/goldmark v1.4.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
||||
replace golang.org/x/crypto => github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03
|
||||
|
||||
replace github.com/henvic/httpretty v0.0.6 => github.com/mislav/httpretty v0.1.1-0.20230202151216-d31343e0d884
|
||||
|
|
|
|||
441
go.sum
441
go.sum
|
|
@ -1,40 +1,5 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
|
|
@ -47,48 +12,39 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
|
|||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
|
||||
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da h1:FGz53GWQRiKQ/5xUsoCCkewSQIC7u81Scaxx2nUy3nM=
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da/go.mod h1:HXz79SMFnF9arKxqeoHWxmo1BhplAH7wehlRhKQIL94=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
|
||||
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
|
||||
github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg=
|
||||
github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o=
|
||||
github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM=
|
||||
github.com/cli/go-gh/v2 v2.0.1 h1:W4L7C5xT9CwkcqTsUzBhFhRKGpek9oRtdDLku2Hku+E=
|
||||
github.com/cli/go-gh/v2 v2.0.1/go.mod h1:zWab1jRnJ0Ug8qRjsZHFk/Oq51ZWuhSxRL2FDUDgQWk=
|
||||
github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA=
|
||||
github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
|
||||
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk=
|
||||
github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cli/shurcooL-graphql v0.0.3 h1:CtpPxyGDs136/+ZeyAfUKYmcQBjDlq5aqnrDCW5Ghh8=
|
||||
github.com/cli/shurcooL-graphql v0.0.3/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
|
|
@ -97,100 +53,46 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
|||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
|
||||
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/henvic/httpretty v0.1.2 h1:EQo556sO0xeXAjP10eB+BZARMuvkdGqtfeS4Ntjvkiw=
|
||||
github.com/henvic/httpretty v0.1.2/go.mod h1:ViEsly7wgdugYtymX54pYp6Vv2wqZmNHayJ6q8tlKCc=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/itchyny/gojq v0.12.8 h1:Zxcwq8w4IeR8JJYEtoG2MWJZUv0RGY6QqJcO1cqV8+A=
|
||||
github.com/itchyny/gojq v0.12.8/go.mod h1:gE2kZ9fVRU0+JAksaTzjIlgnCa2akU+a1V0WXgJQN5c=
|
||||
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
|
||||
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
||||
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
|
||||
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
|
|
@ -199,8 +101,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
|
|
@ -213,8 +115,6 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyex
|
|||
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
|
||||
github.com/mislav/httpretty v0.1.1-0.20230202151216-d31343e0d884 h1:JQp1j1IWuMQZc2tyDQ9KmksjQbw5MhUOzWzZZn7WyU0=
|
||||
github.com/mislav/httpretty v0.1.1-0.20230202151216-d31343e0d884/go.mod h1:ViEsly7wgdugYtymX54pYp6Vv2wqZmNHayJ6q8tlKCc=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
|
|
@ -224,24 +124,24 @@ github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc
|
|||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00 h1:fiFvD4lT0aWjuuAb64LlZ/67v87m+Kc9Qsu5cMFNK0w=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9 h1:nCBaIs5/R0HFP5+aPW/SzFUF8z0oKuCXmuDmHWaxzjY=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
|
||||
|
|
@ -251,19 +151,17 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
|
|||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
|
|
@ -271,198 +169,55 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5RI/4=
|
||||
github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
|
||||
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
|
|
@ -470,104 +225,20 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package browser
|
|||
import (
|
||||
"io"
|
||||
|
||||
ghBrowser "github.com/cli/go-gh/pkg/browser"
|
||||
ghBrowser "github.com/cli/go-gh/v2/pkg/browser"
|
||||
)
|
||||
|
||||
type Browser interface {
|
||||
|
|
@ -12,5 +12,5 @@ type Browser interface {
|
|||
|
||||
func New(launcher string, stdout, stderr io.Writer) Browser {
|
||||
b := ghBrowser.New(launcher, stdout, stderr)
|
||||
return &b
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
|
|
@ -16,4 +17,11 @@ func init() {
|
|||
Version = info.Main.Version
|
||||
}
|
||||
}
|
||||
|
||||
// Signal the tcell library to skip its expensive `init` block. This saves 30-40ms in startup
|
||||
// time for the gh process. The downside is that some Unicode glyphs from user-generated
|
||||
// content might cause mis-alignment in tcell-enabled views.
|
||||
//
|
||||
// https://github.com/gdamore/tcell/commit/2f889d79bd61b1fd2f43372529975a65b792a7ae
|
||||
_ = os.Setenv("TCELL_MINIMIZE", "1")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,17 +122,24 @@ func (a *API) GetUser(ctx context.Context) (*User, error) {
|
|||
|
||||
var response User
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// RepositoryOwner represents owner of a repository
|
||||
type RepositoryOwner struct {
|
||||
Type string `json:"type"`
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
// Repository represents a GitHub repository.
|
||||
type Repository struct {
|
||||
ID int `json:"id"`
|
||||
FullName string `json:"full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
ID int `json:"id"`
|
||||
FullName string `json:"full_name"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
Owner RepositoryOwner `json:"owner"`
|
||||
}
|
||||
|
||||
// GetRepository returns the repository associated with the given owner and name.
|
||||
|
|
@ -160,7 +167,7 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
|
|||
|
||||
var response Repository
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
|
|
@ -185,6 +192,15 @@ type Codespace struct {
|
|||
PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"`
|
||||
IdleTimeoutNotice string `json:"idle_timeout_notice"`
|
||||
WebURL string `json:"web_url"`
|
||||
DevContainerPath string `json:"devcontainer_path"`
|
||||
Prebuild bool `json:"prebuild"`
|
||||
Location string `json:"location"`
|
||||
IdleTimeoutMinutes int `json:"idle_timeout_minutes"`
|
||||
RetentionPeriodMinutes int `json:"retention_period_minutes"`
|
||||
RetentionExpiresAt string `json:"retention_expires_at"`
|
||||
RecentFolders []string `json:"recent_folders"`
|
||||
BillableOwner User `json:"billable_owner"`
|
||||
EnvironmentId string `json:"environment_id"`
|
||||
}
|
||||
|
||||
type CodespaceGitStatus struct {
|
||||
|
|
@ -223,8 +239,8 @@ type CodespaceConnection struct {
|
|||
HostPublicKeys []string `json:"hostPublicKeys"`
|
||||
}
|
||||
|
||||
// CodespaceFields is the list of exportable fields for a codespace.
|
||||
var CodespaceFields = []string{
|
||||
// ListCodespaceFields is the list of exportable fields for a codespace when using the `gh cs list` command.
|
||||
var ListCodespaceFields = []string{
|
||||
"displayName",
|
||||
"name",
|
||||
"owner",
|
||||
|
|
@ -237,6 +253,30 @@ var CodespaceFields = []string{
|
|||
"vscsTarget",
|
||||
}
|
||||
|
||||
// ViewCodespaceFields is the list of exportable fields for a codespace when using the `gh cs view` command.
|
||||
var ViewCodespaceFields = []string{
|
||||
"name",
|
||||
"displayName",
|
||||
"state",
|
||||
"owner",
|
||||
"billableOwner",
|
||||
"location",
|
||||
"repository",
|
||||
"gitStatus",
|
||||
"devcontainerPath",
|
||||
"machineName",
|
||||
"machineDisplayName",
|
||||
"prebuild",
|
||||
"createdAt",
|
||||
"lastUsedAt",
|
||||
"idleTimeoutMinutes",
|
||||
"retentionPeriodDays",
|
||||
"retentionExpiresAt",
|
||||
"recentFolders",
|
||||
"vscsTarget",
|
||||
"environmentId",
|
||||
}
|
||||
|
||||
func (c *Codespace) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
|
@ -249,11 +289,17 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} {
|
|||
data[f] = c.Repository.FullName
|
||||
case "machineName":
|
||||
data[f] = c.Machine.Name
|
||||
case "machineDisplayName":
|
||||
data[f] = c.Machine.DisplayName
|
||||
case "retentionPeriodDays":
|
||||
data[f] = c.RetentionPeriodMinutes / 1440
|
||||
case "gitStatus":
|
||||
data[f] = map[string]interface{}{
|
||||
"ref": c.GitStatus.Ref,
|
||||
"hasUnpushedChanges": c.GitStatus.HasUnpushedChanges,
|
||||
"hasUncommittedChanges": c.GitStatus.HasUncommittedChanges,
|
||||
"ahead": c.GitStatus.Ahead,
|
||||
"behind": c.GitStatus.Behind,
|
||||
}
|
||||
case "vscsTarget":
|
||||
if c.VSCSTarget != "" && c.VSCSTarget != VSCSTargetProduction {
|
||||
|
|
@ -336,7 +382,7 @@ func (a *API) ListCodespaces(ctx context.Context, opts ListCodespacesOptions) (c
|
|||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
nextURL := findNextPage(resp.Header.Get("Link"))
|
||||
|
|
@ -398,7 +444,7 @@ func (a *API) GetOrgMemberCodespace(ctx context.Context, orgName string, userNam
|
|||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
for _, cs := range response.Codespaces {
|
||||
|
|
@ -455,7 +501,7 @@ func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeCon
|
|||
|
||||
var response Codespace
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
|
|
@ -563,7 +609,7 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc
|
|||
Machines []*Machine `json:"machines"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return response.Machines, nil
|
||||
|
|
@ -636,7 +682,7 @@ func (a *API) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch str
|
|||
Items []*Repository `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
repoNames := make([]string, len(response.Items))
|
||||
|
|
@ -683,7 +729,7 @@ func (a *API) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*User,
|
|||
}
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
// While this response contains further helpful information ahead of codespace creation,
|
||||
|
|
@ -815,7 +861,7 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
|
|||
|
||||
var response Codespace
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, errProvisioningInProgress // RPC finished before result of creation known
|
||||
|
|
@ -831,7 +877,7 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
|
|||
return nil, fmt.Errorf("error reading response body: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &ue); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
if ue.AllowPermissionsURL != "" {
|
||||
|
|
@ -853,7 +899,7 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
|
|||
|
||||
var response Codespace
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
|
|
@ -934,7 +980,7 @@ func (a *API) ListDevContainers(ctx context.Context, repoID int, branch string,
|
|||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
if err := dec.Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
nextURL := findNextPage(resp.Header.Get("Link"))
|
||||
|
|
@ -1007,7 +1053,7 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E
|
|||
|
||||
var response Codespace
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
|
|
@ -1055,7 +1101,7 @@ func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Cod
|
|||
|
||||
var response getCodespaceRepositoryContentsResponse
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %w", err)
|
||||
return nil, fmt.Errorf("error unmarshalling response: %w", err)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(response.Content)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func generateCodespaceList(start int, end int) []*Codespace {
|
|||
return codespacesList
|
||||
}
|
||||
|
||||
func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int) *httptest.Server {
|
||||
func createFakeListEndpointServer(t *testing.T, initialTotal int, finalTotal int) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/user/codespaces" {
|
||||
t.Fatal("Incorrect path")
|
||||
|
|
@ -48,7 +48,7 @@ func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int)
|
|||
switch page {
|
||||
case 1:
|
||||
response.Codespaces = generateCodespaceList(0, per_page)
|
||||
response.TotalCount = initalTotal
|
||||
response.TotalCount = initialTotal
|
||||
w.Header().Set("Link", fmt.Sprintf(`<http://%[1]s/user/codespaces?page=3&per_page=%[2]d>; rel="last", <http://%[1]s/user/codespaces?page=2&per_page=%[2]d>; rel="next"`, r.Host, per_page))
|
||||
case 2:
|
||||
response.Codespaces = generateCodespaceList(per_page, per_page*2)
|
||||
|
|
|
|||
|
|
@ -88,9 +88,14 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
|
|||
})
|
||||
}
|
||||
|
||||
// ListenTCP starts a localhost tcp listener and returns the listener and bound port
|
||||
func ListenTCP(port int) (*net.TCPListener, int, error) {
|
||||
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
// ListenTCP starts a localhost tcp listener on 127.0.0.1 (unless allInterfaces is true) and returns the listener and bound port
|
||||
func ListenTCP(port int, allInterfaces bool) (*net.TCPListener, int, error) {
|
||||
host := "127.0.0.1"
|
||||
if allInterfaces {
|
||||
host = ""
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to build tcp address: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.21.12
|
||||
// protoc v3.12.4
|
||||
// source: codespace/codespace_host_service.v1.proto
|
||||
|
||||
package codespace
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.12
|
||||
// - protoc v3.12.4
|
||||
// source: codespace/codespace_host_service.v1.proto
|
||||
|
||||
package codespace
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ function generate {
|
|||
|
||||
local contract="$dir/$proto"
|
||||
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative "$contract"
|
||||
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative "$contract" --experimental_allow_proto3_optional
|
||||
echo "Generated protocol buffers for $contract"
|
||||
|
||||
services=$(cat "$contract" | grep -Eo "service .+ {" | awk '{print $2 "Server"}')
|
||||
moq -out $contract.mock.go $dir $services
|
||||
services=$(grep -Eo "service .+ {" <$contract | awk '{print $2 "Server"}')
|
||||
moq -out "$contract.mock.go" "$dir" "$services"
|
||||
echo "Generated mock protocols for $contract"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ func (i *invoker) StartSSHServerWithOptions(ctx context.Context, options StartSS
|
|||
}
|
||||
|
||||
func listenTCP() (*net.TCPListener, error) {
|
||||
// We will end up using this same address to connect, so specify the IP also or the connect will fail
|
||||
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build tcp address: %w", err)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.21.12
|
||||
// protoc v3.12.4
|
||||
// source: jupyter/jupyter_server_host_service.v1.proto
|
||||
|
||||
package jupyter
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.12
|
||||
// - protoc v3.12.4
|
||||
// source: jupyter/jupyter_server_host_service.v1.proto
|
||||
|
||||
package jupyter
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.21.12
|
||||
// protoc v3.12.4
|
||||
// source: ssh/ssh_server_host_service.v1.proto
|
||||
|
||||
package ssh
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.12
|
||||
// - protoc v3.12.4
|
||||
// source: ssh/ssh_server_host_service.v1.proto
|
||||
|
||||
package ssh
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@ type printer interface {
|
|||
// Shell runs an interactive secure shell over an existing
|
||||
// port-forwarding session. It runs until the shell is terminated
|
||||
// (including by cancellation of the context).
|
||||
func Shell(ctx context.Context, p printer, sshArgs []string, port int, destination string, usingCustomPort bool) error {
|
||||
func Shell(
|
||||
ctx context.Context, p printer, sshArgs []string, port int, destination string, printConnDetails bool,
|
||||
) error {
|
||||
cmd, connArgs, err := newSSHCommand(ctx, port, destination, sshArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ssh command: %w", err)
|
||||
}
|
||||
|
||||
if usingCustomPort {
|
||||
if printConnDetails {
|
||||
p.Printf("Connection Details: ssh %s %s", destination, connArgs)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func PollPostCreateStates(ctx context.Context, progress progressIndicator, apiCl
|
|||
}()
|
||||
|
||||
// Ensure local port is listening before client (getPostCreateOutput) connects.
|
||||
listen, localPort, err := ListenTCP(0)
|
||||
listen, localPort, err := ListenTCP(0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
ghAuth "github.com/cli/go-gh/pkg/auth"
|
||||
ghConfig "github.com/cli/go-gh/pkg/config"
|
||||
"github.com/zalando/go-keyring"
|
||||
"github.com/cli/cli/v2/internal/keyring"
|
||||
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
ghConfig "github.com/cli/go-gh/v2/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -124,7 +124,7 @@ type AuthConfig struct {
|
|||
|
||||
// Token will retrieve the auth token for the given hostname,
|
||||
// searching environment variables, plain text config, and
|
||||
// lastly encypted storage.
|
||||
// lastly encrypted storage.
|
||||
func (c *AuthConfig) Token(hostname string) (string, string) {
|
||||
if c.tokenOverride != nil {
|
||||
return c.tokenOverride(hostname)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
ghConfig "github.com/cli/go-gh/pkg/config"
|
||||
ghConfig "github.com/cli/go-gh/v2/pkg/config"
|
||||
)
|
||||
|
||||
func NewBlankConfig() *ConfigMock {
|
||||
|
|
|
|||
|
|
@ -22,30 +22,25 @@ var allIssueFeatures = IssueFeatures{
|
|||
}
|
||||
|
||||
type PullRequestFeatures struct {
|
||||
ReviewDecision bool
|
||||
StatusCheckRollup bool
|
||||
BranchProtectionRule bool
|
||||
MergeQueue bool
|
||||
MergeQueue bool
|
||||
// CheckRunAndStatusContextCounts indicates whether the API supports
|
||||
// the checkRunCount, checkRunCountsByState, statusContextCount and stausContextCountsByState
|
||||
// fields on the StatusCheckRollupContextConnection
|
||||
CheckRunAndStatusContextCounts bool
|
||||
}
|
||||
|
||||
var allPullRequestFeatures = PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
}
|
||||
|
||||
type RepositoryFeatures struct {
|
||||
IssueTemplateMutation bool
|
||||
IssueTemplateQuery bool
|
||||
PullRequestTemplateQuery bool
|
||||
VisibilityField bool
|
||||
AutoMerge bool
|
||||
}
|
||||
|
||||
var allRepositoryFeatures = RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
AutoMerge: true,
|
||||
|
|
@ -103,32 +98,40 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
|
|||
// return allPullRequestFeatures, nil
|
||||
// }
|
||||
|
||||
features := PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
}
|
||||
|
||||
var featureDetection struct {
|
||||
var pullRequestFeatureDetection struct {
|
||||
PullRequest struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
|
||||
StatusCheckRollupContextConnection struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"StatusCheckRollupContextConnection: __type(name: \"StatusCheckRollupContextConnection\")"`
|
||||
}
|
||||
|
||||
gql := api.NewClientFromHTTP(d.httpClient)
|
||||
err := gql.Query(d.host, "PullRequest_fields", &featureDetection, nil)
|
||||
if err != nil {
|
||||
return features, err
|
||||
if err := gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil); err != nil {
|
||||
return PullRequestFeatures{}, err
|
||||
}
|
||||
|
||||
for _, field := range featureDetection.PullRequest.Fields {
|
||||
features := PullRequestFeatures{}
|
||||
|
||||
for _, field := range pullRequestFeatureDetection.PullRequest.Fields {
|
||||
if field.Name == "isInMergeQueue" {
|
||||
features.MergeQueue = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range pullRequestFeatureDetection.StatusCheckRollupContextConnection.Fields {
|
||||
// We only check for checkRunCount here but it, checkRunCountsByState, statusContextCount and statusContextCountsByState
|
||||
// were all introduced in the same version of the API.
|
||||
if field.Name == "checkRunCount" {
|
||||
features.CheckRunAndStatusContextCounts = true
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
|
|
@ -137,10 +140,7 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
|
|||
return allRepositoryFeatures, nil
|
||||
}
|
||||
|
||||
features := RepositoryFeatures{
|
||||
IssueTemplateQuery: true,
|
||||
IssueTemplateMutation: true,
|
||||
}
|
||||
features := RepositoryFeatures{}
|
||||
|
||||
var featureDetection struct {
|
||||
Repository struct {
|
||||
|
|
|
|||
|
|
@ -82,21 +82,32 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "github.com",
|
||||
name: "github.com with merge queue and status check counts by state",
|
||||
hostname: "github.com",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
]
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -105,34 +116,77 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
hostname: "github.com",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": []
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: false,
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE",
|
||||
name: "GHE with merge queue and status check counts by state",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
]
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE without merge queue and status check counts by state",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": []
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -169,8 +223,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
AutoMerge: true,
|
||||
|
|
@ -184,8 +236,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`query Repository_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: false,
|
||||
},
|
||||
wantErr: false,
|
||||
|
|
@ -201,8 +251,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
},
|
||||
wantErr: false,
|
||||
|
|
@ -218,9 +266,7 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
VisibilityField: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -235,9 +281,7 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
AutoMerge: true,
|
||||
AutoMerge: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,37 +6,56 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// DefaultHostname is the domain name of the default GitHub instance.
|
||||
const defaultHostname = "github.com"
|
||||
|
||||
// localhost is the domain name of a local GitHub instance
|
||||
// Localhost is the domain name of a local GitHub instance.
|
||||
const localhost = "github.localhost"
|
||||
|
||||
// Default returns the host name of the default GitHub instance
|
||||
// TenancyHost is the domain name of a tenancy GitHub instance.
|
||||
const tenancyHost = "ghe.com"
|
||||
|
||||
// Default returns the host name of the default GitHub instance.
|
||||
func Default() string {
|
||||
return defaultHostname
|
||||
}
|
||||
|
||||
// IsEnterprise reports whether a non-normalized host name looks like a GHE instance
|
||||
// IsEnterprise reports whether a non-normalized host name looks like a GHE instance.
|
||||
func IsEnterprise(h string) bool {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
return normalizedHostName != defaultHostname && normalizedHostName != localhost
|
||||
}
|
||||
|
||||
// IsTenancy reports whether a non-normalized host name looks like a tenancy instance.
|
||||
func IsTenancy(h string) bool {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
return strings.HasSuffix(normalizedHostName, "."+tenancyHost)
|
||||
}
|
||||
|
||||
// TenantName extracts the tenant name from tenancy host name and
|
||||
// reports whether it found the tenant name.
|
||||
func TenantName(h string) (string, bool) {
|
||||
normalizedHostName := NormalizeHostname(h)
|
||||
return cutSuffix(normalizedHostName, "."+tenancyHost)
|
||||
}
|
||||
|
||||
func isGarage(h string) bool {
|
||||
return strings.EqualFold(h, "garage.github.com")
|
||||
}
|
||||
|
||||
// NormalizeHostname returns the canonical host name of a GitHub instance
|
||||
// NormalizeHostname returns the canonical host name of a GitHub instance.
|
||||
func NormalizeHostname(h string) string {
|
||||
hostname := strings.ToLower(h)
|
||||
if strings.HasSuffix(hostname, "."+defaultHostname) {
|
||||
return defaultHostname
|
||||
}
|
||||
|
||||
if strings.HasSuffix(hostname, "."+localhost) {
|
||||
return localhost
|
||||
}
|
||||
|
||||
if before, found := cutSuffix(hostname, "."+tenancyHost); found {
|
||||
idx := strings.LastIndex(before, ".")
|
||||
return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost)
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +97,9 @@ func RESTPrefix(hostname string) string {
|
|||
|
||||
func GistPrefix(hostname string) string {
|
||||
prefix := "https://"
|
||||
|
||||
if strings.EqualFold(hostname, localhost) {
|
||||
prefix = "http://"
|
||||
}
|
||||
|
||||
return prefix + GistHost(hostname)
|
||||
}
|
||||
|
||||
|
|
@ -105,3 +122,11 @@ func HostPrefix(hostname string) string {
|
|||
}
|
||||
return fmt.Sprintf("https://%s/", hostname)
|
||||
}
|
||||
|
||||
// Backport strings.CutSuffix from Go 1.20.
|
||||
func cutSuffix(s, suffix string) (string, bool) {
|
||||
if !strings.HasSuffix(s, suffix) {
|
||||
return s, false
|
||||
}
|
||||
return s[:len(s)-len(suffix)], true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,87 @@ func TestIsEnterprise(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsTenancy(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
host: "github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "github.localhost",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "garage.github.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "ghe.com",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
host: "api.tenant.ghe.com",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
if got := IsTenancy(tt.host); got != tt.want {
|
||||
t.Errorf("IsTenancy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantName(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
wantTenant string
|
||||
wantFound bool
|
||||
}{
|
||||
{
|
||||
host: "github.com",
|
||||
wantTenant: "github.com",
|
||||
},
|
||||
{
|
||||
host: "github.localhost",
|
||||
wantTenant: "github.localhost",
|
||||
},
|
||||
{
|
||||
host: "garage.github.com",
|
||||
wantTenant: "github.com",
|
||||
},
|
||||
{
|
||||
host: "ghe.com",
|
||||
wantTenant: "ghe.com",
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
wantTenant: "tenant",
|
||||
wantFound: true,
|
||||
},
|
||||
{
|
||||
host: "api.tenant.ghe.com",
|
||||
wantTenant: "tenant",
|
||||
wantFound: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
if tenant, found := TenantName(tt.host); tenant != tt.wantTenant || found != tt.wantFound {
|
||||
t.Errorf("TenantName(%v) = %v %v, want %v %v", tt.host, tenant, found, tt.wantTenant, tt.wantFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeHostname(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
|
|
@ -90,6 +171,18 @@ func TestNormalizeHostname(t *testing.T) {
|
|||
host: "git.my.org",
|
||||
want: "git.my.org",
|
||||
},
|
||||
{
|
||||
host: "ghe.com",
|
||||
want: "ghe.com",
|
||||
},
|
||||
{
|
||||
host: "tenant.ghe.com",
|
||||
want: "tenant.ghe.com",
|
||||
},
|
||||
{
|
||||
host: "api.tenant.ghe.com",
|
||||
want: "tenant.ghe.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
|
|
@ -139,6 +232,7 @@ func TestHostnameValidator(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphQLEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
ghAuth "github.com/cli/go-gh/pkg/auth"
|
||||
"github.com/cli/go-gh/pkg/repository"
|
||||
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/cli/go-gh/v2/pkg/repository"
|
||||
)
|
||||
|
||||
// Interface describes an object that represents a GitHub repository
|
||||
|
|
@ -54,7 +54,7 @@ func FromFullNameWithHost(nwo, fallbackHost string) (Interface, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewWithHost(repo.Owner(), repo.Name(), repo.Host()), nil
|
||||
return NewWithHost(repo.Owner, repo.Name, repo.Host), nil
|
||||
}
|
||||
|
||||
// FromURL extracts the GitHub repository information from a git remote URL
|
||||
|
|
@ -92,12 +92,13 @@ func GenerateRepoURL(repo Interface, p string, args ...interface{}) string {
|
|||
return baseURL
|
||||
}
|
||||
|
||||
// TODO there is a parallel implementation for non-isolated commands
|
||||
func FormatRemoteURL(repo Interface, protocol string) string {
|
||||
if protocol == "ssh" {
|
||||
if tenant, found := ghinstance.TenantName(repo.RepoHost()); found {
|
||||
return fmt.Sprintf("%s@%s:%s/%s.git", tenant, repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s/%s.git", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,3 +220,59 @@ func TestFromFullName(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRemoteURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repoHost string
|
||||
repoOwner string
|
||||
repoName string
|
||||
protocol string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "https protocol",
|
||||
repoHost: "github.com",
|
||||
repoOwner: "owner",
|
||||
repoName: "name",
|
||||
protocol: "https",
|
||||
want: "https://github.com/owner/name.git",
|
||||
},
|
||||
{
|
||||
name: "https protocol local host",
|
||||
repoHost: "github.localhost",
|
||||
repoOwner: "owner",
|
||||
repoName: "name",
|
||||
protocol: "https",
|
||||
want: "http://github.localhost/owner/name.git",
|
||||
},
|
||||
{
|
||||
name: "ssh protocol",
|
||||
repoHost: "github.com",
|
||||
repoOwner: "owner",
|
||||
repoName: "name",
|
||||
protocol: "ssh",
|
||||
want: "git@github.com:owner/name.git",
|
||||
},
|
||||
{
|
||||
name: "ssh protocol tenancy host",
|
||||
repoHost: "tenant.ghe.com",
|
||||
repoOwner: "owner",
|
||||
repoName: "name",
|
||||
protocol: "ssh",
|
||||
want: "tenant@tenant.ghe.com:owner/name.git",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := ghRepo{
|
||||
hostname: tt.repoHost,
|
||||
owner: tt.repoOwner,
|
||||
name: tt.repoName,
|
||||
}
|
||||
if url := FormatRemoteURL(r, tt.protocol); url != tt.want {
|
||||
t.Errorf("expected url %q, got %q", tt.want, url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
internal/keyring/keyring.go
Normal file
68
internal/keyring/keyring.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Package keyring is a simple wrapper that adds timeouts to the zalando/go-keyring package.
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
type TimeoutError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Set secret in keyring for user.
|
||||
func Set(service, user, secret string) error {
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
ch <- keyring.Set(service, user, secret)
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-time.After(3 * time.Second):
|
||||
return &TimeoutError{"timeout while trying to set secret in keyring"}
|
||||
}
|
||||
}
|
||||
|
||||
// Get secret from keyring given service and user name.
|
||||
func Get(service, user string) (string, error) {
|
||||
ch := make(chan struct {
|
||||
val string
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
val, err := keyring.Get(service, user)
|
||||
ch <- struct {
|
||||
val string
|
||||
err error
|
||||
}{val, err}
|
||||
}()
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res.val, res.err
|
||||
case <-time.After(3 * time.Second):
|
||||
return "", &TimeoutError{"timeout while trying to get secret from keyring"}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete secret from keyring.
|
||||
func Delete(service, user string) error {
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
ch <- keyring.Delete(service, user)
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-time.After(3 * time.Second):
|
||||
return &TimeoutError{"timeout while trying to delete secret from keyring"}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,14 @@ import (
|
|||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
)
|
||||
|
||||
//go:generate moq -rm -out prompter_mock.go . Prompter
|
||||
type Prompter interface {
|
||||
Select(string, string, []string) (int, error)
|
||||
MultiSelect(string, string, []string) ([]string, error)
|
||||
MultiSelect(string, []string, []string) ([]int, error)
|
||||
Input(string, string) (string, error)
|
||||
InputHostname() (string, error)
|
||||
Password(string) (string, error)
|
||||
|
|
@ -49,11 +50,24 @@ type surveyPrompter struct {
|
|||
stderr io.Writer
|
||||
}
|
||||
|
||||
// LatinMatchingFilter returns whether the value matches the input filter.
|
||||
// The strings are compared normalized in case.
|
||||
// The filter's diactritics are kept as-is, but the value's are normalized,
|
||||
// so that a missing diactritic in the filter still returns a result.
|
||||
func LatinMatchingFilter(filter, value string, index int) bool {
|
||||
filter = strings.ToLower(filter)
|
||||
value = strings.ToLower(value)
|
||||
|
||||
// include this option if it matches.
|
||||
return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter)
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) Select(message, defaultValue string, options []string) (result int, err error) {
|
||||
q := &survey.Select{
|
||||
Message: message,
|
||||
Options: options,
|
||||
PageSize: 20,
|
||||
Filter: LatinMatchingFilter,
|
||||
}
|
||||
|
||||
if defaultValue != "" {
|
||||
|
|
@ -72,15 +86,25 @@ func (p *surveyPrompter) Select(message, defaultValue string, options []string)
|
|||
return
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) MultiSelect(message, defaultValue string, options []string) (result []string, err error) {
|
||||
func (p *surveyPrompter) MultiSelect(message string, defaultValues, options []string) (result []int, err error) {
|
||||
q := &survey.MultiSelect{
|
||||
Message: message,
|
||||
Options: options,
|
||||
PageSize: 20,
|
||||
Filter: LatinMatchingFilter,
|
||||
}
|
||||
|
||||
if defaultValue != "" {
|
||||
q.Default = defaultValue
|
||||
if len(defaultValues) > 0 {
|
||||
// TODO I don't actually know that this is needed, just being extra cautious
|
||||
validatedDefault := []string{}
|
||||
for _, x := range defaultValues {
|
||||
for _, y := range options {
|
||||
if x == y {
|
||||
validatedDefault = append(validatedDefault, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Default = validatedDefault
|
||||
}
|
||||
|
||||
err = p.ask(q, &result)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ var _ Prompter = &PrompterMock{}
|
|||
// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
|
||||
// panic("mock out the MarkdownEditor method")
|
||||
// },
|
||||
// MultiSelectFunc: func(s1 string, s2 string, strings []string) ([]string, error) {
|
||||
// MultiSelectFunc: func(s string, strings1 []string, strings2 []string) ([]int, error) {
|
||||
// panic("mock out the MultiSelect method")
|
||||
// },
|
||||
// PasswordFunc: func(s string) (string, error) {
|
||||
|
|
@ -70,7 +70,7 @@ type PrompterMock struct {
|
|||
MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
|
||||
|
||||
// MultiSelectFunc mocks the MultiSelect method.
|
||||
MultiSelectFunc func(s1 string, s2 string, strings []string) ([]string, error)
|
||||
MultiSelectFunc func(s string, strings1 []string, strings2 []string) ([]int, error)
|
||||
|
||||
// PasswordFunc mocks the Password method.
|
||||
PasswordFunc func(s string) (string, error)
|
||||
|
|
@ -116,12 +116,12 @@ type PrompterMock struct {
|
|||
}
|
||||
// MultiSelect holds details about calls to the MultiSelect method.
|
||||
MultiSelect []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
// Strings is the strings argument value.
|
||||
Strings []string
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// Strings1 is the strings1 argument value.
|
||||
Strings1 []string
|
||||
// Strings2 is the strings2 argument value.
|
||||
Strings2 []string
|
||||
}
|
||||
// Password holds details about calls to the Password method.
|
||||
Password []struct {
|
||||
|
|
@ -348,23 +348,23 @@ func (mock *PrompterMock) MarkdownEditorCalls() []struct {
|
|||
}
|
||||
|
||||
// MultiSelect calls MultiSelectFunc.
|
||||
func (mock *PrompterMock) MultiSelect(s1 string, s2 string, strings []string) ([]string, error) {
|
||||
func (mock *PrompterMock) MultiSelect(s string, strings1 []string, strings2 []string) ([]int, error) {
|
||||
if mock.MultiSelectFunc == nil {
|
||||
panic("PrompterMock.MultiSelectFunc: method is nil but Prompter.MultiSelect was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
Strings: strings,
|
||||
S: s,
|
||||
Strings1: strings1,
|
||||
Strings2: strings2,
|
||||
}
|
||||
mock.lockMultiSelect.Lock()
|
||||
mock.calls.MultiSelect = append(mock.calls.MultiSelect, callInfo)
|
||||
mock.lockMultiSelect.Unlock()
|
||||
return mock.MultiSelectFunc(s1, s2, strings)
|
||||
return mock.MultiSelectFunc(s, strings1, strings2)
|
||||
}
|
||||
|
||||
// MultiSelectCalls gets all the calls that were made to MultiSelect.
|
||||
|
|
@ -372,14 +372,14 @@ func (mock *PrompterMock) MultiSelect(s1 string, s2 string, strings []string) ([
|
|||
//
|
||||
// len(mockedPrompter.MultiSelectCalls())
|
||||
func (mock *PrompterMock) MultiSelectCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
}
|
||||
mock.lockMultiSelect.RLock()
|
||||
calls = mock.calls.MultiSelect
|
||||
|
|
|
|||
78
internal/prompter/prompter_test.go
Normal file
78
internal/prompter/prompter_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package prompter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterDiacritics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter string
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exact match no diacritics",
|
||||
filter: "Mikelis",
|
||||
value: "Mikelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match no diacritics",
|
||||
filter: "Mikelis",
|
||||
value: "Mikelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match diacritics",
|
||||
filter: "Miķelis",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "partial match diacritics",
|
||||
filter: "Miķe",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact match diacritics in value",
|
||||
filter: "Mikelis",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "partial match diacritics in filter",
|
||||
filter: "Miķe",
|
||||
value: "Miķelis",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match when removing diacritics in filter",
|
||||
filter: "Mielis",
|
||||
value: "Mikelis",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no match when removing diacritics in value",
|
||||
filter: "Mikelis",
|
||||
value: "Mielis",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no match diacritics in filter",
|
||||
filter: "Miķelis",
|
||||
value: "Mikelis",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, LatinMatchingFilter(tt.filter, tt.value, 0), tt.want)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ type ConfirmStub struct {
|
|||
type MultiSelectStub struct {
|
||||
Prompt string
|
||||
ExpectedOpts []string
|
||||
Fn func(string, string, []string) ([]string, error)
|
||||
Fn func(string, []string, []string) ([]int, error)
|
||||
}
|
||||
|
||||
type InputHostnameStub struct {
|
||||
|
|
@ -84,8 +84,6 @@ type MockPrompter struct {
|
|||
ConfirmDeletionStubs []ConfirmDeletionStub
|
||||
}
|
||||
|
||||
// TODO thread safety
|
||||
|
||||
func NewMockPrompter(t *testing.T) *MockPrompter {
|
||||
m := &MockPrompter{
|
||||
t: t,
|
||||
|
|
@ -120,17 +118,17 @@ func NewMockPrompter(t *testing.T) *MockPrompter {
|
|||
return s.Fn(p, d, opts)
|
||||
}
|
||||
|
||||
m.MultiSelectFunc = func(p, d string, opts []string) ([]string, error) {
|
||||
m.MultiSelectFunc = func(p string, d, opts []string) ([]int, error) {
|
||||
var s MultiSelectStub
|
||||
if len(m.SelectStubs) > 0 {
|
||||
if len(m.MultiSelectStubs) > 0 {
|
||||
s = m.MultiSelectStubs[0]
|
||||
m.SelectStubs = m.SelectStubs[1:len(m.SelectStubs)]
|
||||
m.MultiSelectStubs = m.MultiSelectStubs[1:len(m.MultiSelectStubs)]
|
||||
} else {
|
||||
return []string{}, NoSuchPromptErr(p)
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return []string{}, NoSuchPromptErr(p)
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
AssertOptions(m.t, s.ExpectedOpts, opts)
|
||||
|
|
@ -240,7 +238,7 @@ func (m *MockPrompter) RegisterSelect(prompt string, opts []string, stub func(_,
|
|||
Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterMultiSelect(prompt string, opts []string, stub func(_, _ string, _ []string) ([]string, error)) {
|
||||
func (m *MockPrompter) RegisterMultiSelect(prompt string, d, opts []string, stub func(_ string, _, _ []string) ([]int, error)) {
|
||||
m.MultiSelectStubs = append(m.MultiSelectStubs, MultiSelectStub{
|
||||
Prompt: prompt,
|
||||
ExpectedOpts: opts,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/go-gh/pkg/tableprinter"
|
||||
"github.com/cli/go-gh/v2/pkg/tableprinter"
|
||||
)
|
||||
|
||||
type TablePrinter struct {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/cli/go-gh/pkg/text"
|
||||
"github.com/cli/go-gh/v2/pkg/text"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/runes"
|
||||
"golang.org/x/text/transform"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
var whitespaceRE = regexp.MustCompile(`\s+`)
|
||||
|
|
@ -72,3 +76,19 @@ func DisplayURL(urlStr string) string {
|
|||
}
|
||||
return u.Hostname() + u.Path
|
||||
}
|
||||
|
||||
// RemoveDiacritics returns the input value without "diacritics", or accent marks
|
||||
func RemoveDiacritics(value string) string {
|
||||
// Mn = "Mark, nonspacing" unicode character category
|
||||
removeMnTransfomer := runes.Remove(runes.In(unicode.Mn))
|
||||
|
||||
// 1/ Decompose the text into characters and diacritical marks,
|
||||
// 2/ Remove the diacriticals marks
|
||||
// 3/ Recompose the text
|
||||
t := transform.Chain(norm.NFD, removeMnTransfomer, norm.NFC)
|
||||
normalized, _, err := transform.String(t, value)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,3 +54,57 @@ func TestFuzzyAgoAbbr(t *testing.T) {
|
|||
assert.Equal(t, expected, fuzzy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDiacritics(t *testing.T) {
|
||||
tests := [][]string{
|
||||
// no diacritics
|
||||
{"e", "e"},
|
||||
{"و", "و"},
|
||||
{"И", "И"},
|
||||
{"ж", "ж"},
|
||||
{"私", "私"},
|
||||
{"万", "万"},
|
||||
|
||||
// diacritics test sets
|
||||
{"à", "a"},
|
||||
{"é", "e"},
|
||||
{"è", "e"},
|
||||
{"ô", "o"},
|
||||
{"ᾳ", "α"},
|
||||
{"εͅ", "ε"},
|
||||
{"ῃ", "η"},
|
||||
{"ιͅ", "ι"},
|
||||
|
||||
{"ؤ", "و"},
|
||||
|
||||
{"ā", "a"},
|
||||
{"č", "c"},
|
||||
{"ģ", "g"},
|
||||
{"ķ", "k"},
|
||||
{"ņ", "n"},
|
||||
{"š", "s"},
|
||||
{"ž", "z"},
|
||||
|
||||
{"ŵ", "w"},
|
||||
{"ŷ", "y"},
|
||||
{"ä", "a"},
|
||||
{"ÿ", "y"},
|
||||
{"á", "a"},
|
||||
{"ẁ", "w"},
|
||||
{"ỳ", "y"},
|
||||
{"ō", "o"},
|
||||
|
||||
// full words
|
||||
{"Miķelis", "Mikelis"},
|
||||
{"François", "Francois"},
|
||||
{"žluťoučký", "zlutoucky"},
|
||||
{"învățătorița", "invatatorita"},
|
||||
{"Kękę przy łóżku", "Keke przy łozku"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(RemoveDiacritics(tt[0]), func(t *testing.T) {
|
||||
assert.Equal(t, tt[1], RemoveDiacritics(tt[0]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
To see more help, run 'gh help run <subcommand>'
|
||||
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow list: List workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package alias
|
|||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
deleteCmd "github.com/cli/cli/v2/pkg/cmd/alias/delete"
|
||||
importCmd "github.com/cli/cli/v2/pkg/cmd/alias/imports"
|
||||
listCmd "github.com/cli/cli/v2/pkg/cmd/alias/list"
|
||||
setCmd "github.com/cli/cli/v2/pkg/cmd/alias/set"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -23,6 +24,7 @@ func NewCmdAlias(f *cmdutil.Factory) *cobra.Command {
|
|||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
cmd.AddCommand(deleteCmd.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(importCmd.NewCmdImport(f, nil))
|
||||
cmd.AddCommand(listCmd.NewCmdList(f, nil))
|
||||
cmd.AddCommand(setCmd.NewCmdSet(f, nil))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/findsh"
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
// ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The
|
||||
// second return value indicates whether the alias should be executed in a new shell process instead
|
||||
// of running gh itself.
|
||||
func ExpandAlias(cfg config.Config, args []string, findShFunc func() (string, error)) (expanded []string, isShell bool, err error) {
|
||||
if len(args) < 2 {
|
||||
// the command is lacking a subcommand
|
||||
return
|
||||
}
|
||||
expanded = args[1:]
|
||||
|
||||
aliases := cfg.Aliases()
|
||||
|
||||
expansion, getErr := aliases.Get(args[1])
|
||||
if getErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
isShell = true
|
||||
if findShFunc == nil {
|
||||
findShFunc = findSh
|
||||
}
|
||||
shPath, shErr := findShFunc()
|
||||
if shErr != nil {
|
||||
err = shErr
|
||||
return
|
||||
}
|
||||
|
||||
expanded = []string{shPath, "-c", expansion[1:]}
|
||||
|
||||
if len(args[2:]) > 0 {
|
||||
expanded = append(expanded, "--")
|
||||
expanded = append(expanded, args[2:]...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
extraArgs := []string{}
|
||||
for i, a := range args[2:] {
|
||||
if !strings.Contains(expansion, "$") {
|
||||
extraArgs = append(extraArgs, a)
|
||||
} else {
|
||||
expansion = strings.ReplaceAll(expansion, fmt.Sprintf("$%d", i+1), a)
|
||||
}
|
||||
}
|
||||
lingeringRE := regexp.MustCompile(`\$\d`)
|
||||
if lingeringRE.MatchString(expansion) {
|
||||
err = fmt.Errorf("not enough arguments for alias: %s", expansion)
|
||||
return
|
||||
}
|
||||
|
||||
var newArgs []string
|
||||
newArgs, err = shlex.Split(expansion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expanded = append(newArgs, extraArgs...)
|
||||
return
|
||||
}
|
||||
|
||||
func findSh() (string, error) {
|
||||
shPath, err := findsh.Find()
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
|
||||
}
|
||||
return "", errors.New("unable to locate sh to execute shell alias with")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return shPath, nil
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestExpandAlias(t *testing.T) {
|
||||
findShFunc := func() (string, error) {
|
||||
return "/usr/bin/sh", nil
|
||||
}
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
il: issue list --author="$1" --label="$2"
|
||||
ia: issue list --author="$1" --assignee="$1"
|
||||
`))
|
||||
|
||||
type args struct {
|
||||
config config.Config
|
||||
argv []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantExpanded []string
|
||||
wantIsShell bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "too few arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh"},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "no expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "pr", "status"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "status"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "simple expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "adding arguments after expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co", "123"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout", "123"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="$1" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion 2",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="vilmibm" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "satisfy expansion arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "help wanted"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=help wanted"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "mixed positional and non-positional arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "epic", "-R", "monalisa/testing"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "monalisa/testing"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "dollar in expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "ia", "$coolmoney$"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=$coolmoney$", "--assignee=$coolmoney$"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpanded, gotIsShell, err := ExpandAlias(tt.args.config, tt.args.argv, findShFunc)
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if tt.wantErr.Error() != err.Error() {
|
||||
t.Fatalf("expected error %q, got %q", tt.wantErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotExpanded, tt.wantExpanded) {
|
||||
t.Errorf("ExpandAlias() gotExpanded = %v, want %v", gotExpanded, tt.wantExpanded)
|
||||
}
|
||||
if gotIsShell != tt.wantIsShell {
|
||||
t.Errorf("ExpandAlias() gotIsShell = %v, want %v", gotIsShell, tt.wantIsShell)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cfg := `---
|
||||
// aliases:
|
||||
// co: pr checkout
|
||||
// il: issue list --author="$1" --label="$2"
|
||||
// ia: issue list --author="$1" --assignee="$1"
|
||||
// `
|
||||
// initBlankContext(cfg, "OWNER/REPO", "trunk")
|
||||
// for _, c := range []struct {
|
||||
// Args string
|
||||
// ExpectedArgs []string
|
||||
// Err string
|
||||
// }{
|
||||
// {"gh co", []string{"pr", "checkout"}, ""},
|
||||
// {"gh il", nil, `not enough arguments for alias: issue list --author="$1" --label="$2"`},
|
||||
// {"gh il vilmibm", nil, `not enough arguments for alias: issue list --author="vilmibm" --label="$2"`},
|
||||
// {"gh co 123", []string{"pr", "checkout", "123"}, ""},
|
||||
// {"gh il vilmibm epic", []string{"issue", "list", `--author=vilmibm`, `--label=epic`}, ""},
|
||||
// {"gh ia vilmibm", []string{"issue", "list", `--author=vilmibm`, `--assignee=vilmibm`}, ""},
|
||||
// {"gh ia $coolmoney$", []string{"issue", "list", `--author=$coolmoney$`, `--assignee=$coolmoney$`}, ""},
|
||||
// {"gh pr status", []string{"pr", "status"}, ""},
|
||||
// {"gh il vilmibm epic -R vilmibm/testing", []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "vilmibm/testing"}, ""},
|
||||
// {"gh dne", []string{"dne"}, ""},
|
||||
// {"gh", []string{}, ""},
|
||||
// {"", []string{}, ""},
|
||||
// } {
|
||||
201
pkg/cmd/alias/imports/import.go
Normal file
201
pkg/cmd/alias/imports/import.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
package imports
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ImportOptions struct {
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
Filename string
|
||||
OverwriteExisting bool
|
||||
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command {
|
||||
opts := &ImportOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "import [<filename> | -]",
|
||||
Short: "Import aliases from a YAML file",
|
||||
Long: heredoc.Doc(`
|
||||
Import aliases from the contents of a YAML file.
|
||||
|
||||
Aliases should be defined as a map in YAML, where the keys represent aliases and
|
||||
the values represent the corresponding expansions. An example file should look like
|
||||
the following:
|
||||
|
||||
bugs: issue list --label=bug
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
features: |-
|
||||
issue list
|
||||
--label=enhancement
|
||||
|
||||
Use "-" to read aliases (in YAML format) from standard input.
|
||||
|
||||
The output from the gh command "alias list" can be used to produce a YAML file
|
||||
containing your aliases, which you can use to import them from one machine to
|
||||
another. Run "gh help alias list" to learn more.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# Import aliases from a file
|
||||
$ gh alias import aliases.yml
|
||||
|
||||
# Import aliases from standard input
|
||||
$ gh alias import -
|
||||
`),
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return cmdutil.FlagErrorf("too many arguments")
|
||||
}
|
||||
if len(args) == 0 && opts.IO.IsStdinTTY() {
|
||||
return cmdutil.FlagErrorf("no filename passed and nothing on STDIN")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Filename = "-"
|
||||
if len(args) > 0 {
|
||||
opts.Filename = args[0]
|
||||
}
|
||||
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return importRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Overwrite existing aliases of the same name")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func importRun(opts *ImportOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aliasCfg := cfg.Aliases()
|
||||
|
||||
b, err := cmdutil.ReadFile(opts.Filename, opts.IO.In)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aliasMap := map[string]string{}
|
||||
if err = yaml.Unmarshal(b, &aliasMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
if isTerminal {
|
||||
if opts.Filename == "-" {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "- Importing aliases from standard input\n")
|
||||
} else {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "- Importing aliases from file %q\n", opts.Filename)
|
||||
}
|
||||
}
|
||||
|
||||
var msg strings.Builder
|
||||
|
||||
for _, alias := range getSortedKeys(aliasMap) {
|
||||
var existingAlias bool
|
||||
if _, err := aliasCfg.Get(alias); err == nil {
|
||||
existingAlias = true
|
||||
}
|
||||
|
||||
if !opts.validAliasName(alias) {
|
||||
if !existingAlias {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: already a gh command or extension\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if existingAlias && !opts.OverwriteExisting {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: name already taken\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
expansion := aliasMap[alias]
|
||||
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command, extension, or alias\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
aliasCfg.Add(alias, expansion)
|
||||
|
||||
if existingAlias && opts.OverwriteExisting {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Changed alias %s\n",
|
||||
cs.WarningIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Added alias %s\n",
|
||||
cs.SuccessIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.Write(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isTerminal {
|
||||
fmt.Fprintln(opts.IO.ErrOut, msg.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSortedKeys(m map[string]string) []string {
|
||||
keys := []string{}
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
349
pkg/cmd/alias/imports/import_test.go
Normal file
349
pkg/cmd/alias/imports/import_test.go
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
package imports
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdImport(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
tty bool
|
||||
wants ImportOptions
|
||||
wantsError string
|
||||
}{
|
||||
{
|
||||
name: "no filename and stdin tty",
|
||||
cli: "",
|
||||
tty: true,
|
||||
wants: ImportOptions{
|
||||
Filename: "",
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
wantsError: "no filename passed and nothing on STDIN",
|
||||
},
|
||||
{
|
||||
name: "no filename and stdin is not tty",
|
||||
cli: "",
|
||||
tty: false,
|
||||
wants: ImportOptions{
|
||||
Filename: "-",
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stdin arg",
|
||||
cli: "-",
|
||||
wants: ImportOptions{
|
||||
Filename: "-",
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple filenames",
|
||||
cli: "aliases1 aliases2",
|
||||
wants: ImportOptions{
|
||||
Filename: "aliases1 aliases2",
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
wantsError: "too many arguments",
|
||||
},
|
||||
{
|
||||
name: "clobber flag",
|
||||
cli: "aliases --clobber",
|
||||
wants: ImportOptions{
|
||||
Filename: "aliases",
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
f := &cmdutil.Factory{IOStreams: ios}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *ImportOptions
|
||||
cmd := NewCmdImport(f, func(opts *ImportOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsError != "" {
|
||||
assert.EqualError(t, err, tt.wantsError)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.Filename, gotOpts.Filename)
|
||||
assert.Equal(t, tt.wants.OverwriteExisting, gotOpts.OverwriteExisting)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportRun(t *testing.T) {
|
||||
tmpFile := filepath.Join(t.TempDir(), "aliases.yml")
|
||||
importFileMsg := fmt.Sprintf(`- Importing aliases from file %q`, tmpFile)
|
||||
importStdinMsg := "- Importing aliases from standard input"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *ImportOptions
|
||||
stdin string
|
||||
fileContents string
|
||||
initConfig string
|
||||
aliasCommands []*cobra.Command
|
||||
wantConfig string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "with no existing aliases",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
co: pr checkout
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
wantConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias igrep\n\n",
|
||||
},
|
||||
{
|
||||
name: "with existing aliases",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
users: |-
|
||||
api graphql -F name="$1" -f query='
|
||||
query ($name: String!) {
|
||||
user(login: $name) {
|
||||
name
|
||||
}
|
||||
}'
|
||||
co: pr checkout
|
||||
`),
|
||||
initConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
editor: vim
|
||||
`),
|
||||
aliasCommands: []*cobra.Command{
|
||||
{Use: "igrep"},
|
||||
},
|
||||
wantConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
co: pr checkout
|
||||
users: |-
|
||||
api graphql -F name="$1" -f query='
|
||||
query ($name: String!) {
|
||||
user(login: $name) {
|
||||
name
|
||||
}
|
||||
}'
|
||||
editor: vim
|
||||
`),
|
||||
wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias users\n\n",
|
||||
},
|
||||
{
|
||||
name: "from stdin",
|
||||
opts: &ImportOptions{
|
||||
Filename: "-",
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
stdin: heredoc.Doc(`
|
||||
co: pr checkout
|
||||
features: |-
|
||||
issue list
|
||||
--label=enhancement
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
wantConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
features: |-
|
||||
issue list
|
||||
--label=enhancement
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
wantStderr: importStdinMsg + "\n✓ Added alias co\n✓ Added alias features\n✓ Added alias igrep\n\n",
|
||||
},
|
||||
{
|
||||
name: "already taken aliases",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
co: pr checkout -R cool/repo
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
initConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
editor: vim
|
||||
`),
|
||||
aliasCommands: []*cobra.Command{
|
||||
{Use: "co"},
|
||||
},
|
||||
wantConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
editor: vim
|
||||
`),
|
||||
wantStderr: importFileMsg + "\nX Could not import alias co: name already taken\n✓ Added alias igrep\n\n",
|
||||
},
|
||||
{
|
||||
name: "override aliases",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
co: pr checkout -R cool/repo
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
`),
|
||||
initConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
editor: vim
|
||||
`),
|
||||
aliasCommands: []*cobra.Command{
|
||||
{Use: "co"},
|
||||
},
|
||||
wantConfig: heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout -R cool/repo
|
||||
igrep: '!gh issue list --label="$1" | grep "$2"'
|
||||
editor: vim
|
||||
`),
|
||||
wantStderr: importFileMsg + "\n! Changed alias co\n✓ Added alias igrep\n\n",
|
||||
},
|
||||
{
|
||||
name: "alias is a gh command",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
pr: pr checkout
|
||||
issue: issue list
|
||||
api: api graphql
|
||||
`),
|
||||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias api: already a gh command or extension",
|
||||
"X Could not import alias issue: already a gh command or extension",
|
||||
"X Could not import alias pr: already a gh command or extension\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "invalid expansion",
|
||||
opts: &ImportOptions{
|
||||
Filename: tmpFile,
|
||||
OverwriteExisting: false,
|
||||
},
|
||||
fileContents: heredoc.Doc(`
|
||||
alias1:
|
||||
alias2: ps checkout
|
||||
`),
|
||||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias alias1: expansion does not correspond to a gh command, extension, or alias",
|
||||
"X Could not import alias alias2: expansion does not correspond to a gh command, extension, or alias\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.fileContents != "" {
|
||||
err := os.WriteFile(tmpFile, []byte(tt.fileContents), 0600)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
ios, stdin, _, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
tt.opts.IO = ios
|
||||
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
cfg := config.NewFromString(tt.initConfig)
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Create fake command structure for testing.
|
||||
rootCmd := &cobra.Command{}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
prCmd.AddCommand(&cobra.Command{Use: "status"})
|
||||
rootCmd.AddCommand(prCmd)
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
issueCmd.AddCommand(&cobra.Command{Use: "list"})
|
||||
rootCmd.AddCommand(issueCmd)
|
||||
apiCmd := &cobra.Command{Use: "api"}
|
||||
apiCmd.AddCommand(&cobra.Command{Use: "graphql"})
|
||||
rootCmd.AddCommand(apiCmd)
|
||||
for _, cmd := range tt.aliasCommands {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
|
||||
tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd)
|
||||
tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd)
|
||||
|
||||
if tt.stdin != "" {
|
||||
stdin.WriteString(tt.stdin)
|
||||
}
|
||||
|
||||
err := importRun(tt.opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
configOut := bytes.Buffer{}
|
||||
readConfigs(&configOut, io.Discard)
|
||||
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
assert.Equal(t, tt.wantConfig, configOut.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import (
|
|||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -21,7 +21,8 @@ type SetOptions struct {
|
|||
Expansion string
|
||||
IsShell bool
|
||||
|
||||
validCommand func(string) bool
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
|
||||
|
|
@ -59,6 +60,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
$ gh alias set homework 'issue list --assignee @me'
|
||||
$ gh homework
|
||||
|
||||
$ gh alias set 'issue mine' 'issue list --mention @me'
|
||||
$ gh issue mine
|
||||
|
||||
$ gh alias set epicsBy 'issue list --author="$1" --label="epic"'
|
||||
$ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic"
|
||||
|
||||
|
|
@ -70,29 +74,13 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
opts.Name = args[0]
|
||||
opts.Expansion = args[1]
|
||||
|
||||
opts.validCommand = func(args string) bool {
|
||||
split, err := shlex.Split(args)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, err := rootCmd.Traverse(split)
|
||||
if err == nil && cmd != rootCmd {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, ext := range f.ExtensionManager.List() {
|
||||
if ext.Name() == split[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return setRun(opts)
|
||||
},
|
||||
}
|
||||
|
|
@ -121,18 +109,16 @@ func setRun(opts *SetOptions) error {
|
|||
fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion))
|
||||
}
|
||||
|
||||
isShell := opts.IsShell
|
||||
if isShell && !strings.HasPrefix(expansion, "!") {
|
||||
if opts.IsShell && !strings.HasPrefix(expansion, "!") {
|
||||
expansion = "!" + expansion
|
||||
}
|
||||
isShell = strings.HasPrefix(expansion, "!")
|
||||
|
||||
if opts.validCommand(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name)
|
||||
if !opts.validAliasName(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command, extension, or alias", opts.Name)
|
||||
}
|
||||
|
||||
if !isShell && !opts.validCommand(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion)
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command, extension, or alias", expansion)
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("%s Added alias.", cs.SuccessIcon())
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func runCommand(cfg config.Config, isTTY bool, cli string, in string) (*test.Cmd
|
|||
|
||||
cmd := NewCmdSet(factory, nil)
|
||||
|
||||
// fake command nesting structure needed for validCommand
|
||||
// Create fake command structure for testing.
|
||||
rootCmd := &cobra.Command{}
|
||||
rootCmd.AddCommand(cmd)
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
|
|
@ -73,7 +73,7 @@ func TestAliasSet_gh_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "pr 'pr status'", "")
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command`)
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command, extension, or alias`)
|
||||
}
|
||||
|
||||
func TestAliasSet_empty_aliases(t *testing.T) {
|
||||
|
|
@ -231,7 +231,7 @@ func TestAliasSet_invalid_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "co 'pe checkout'", "")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command, extension, or alias")
|
||||
}
|
||||
|
||||
func TestShellAlias_flag(t *testing.T) {
|
||||
|
|
|
|||
51
pkg/cmd/alias/shared/validations.go
Normal file
51
pkg/cmd/alias/shared/validations.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValidAliasNameFunc returns a function that will check if the given string
|
||||
// is a valid alias name. A name is valid if:
|
||||
// - it does not shadow an existing command,
|
||||
// - it is not nested under a command that is runnable,
|
||||
// - it is not nested under a command that does not exist.
|
||||
func ValidAliasNameFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(args string) bool {
|
||||
split, err := shlex.Split(args)
|
||||
if err != nil || len(split) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
foundCmd, foundArgs, _ := rootCmd.Find(split)
|
||||
if foundCmd != nil && !foundCmd.Runnable() && len(foundArgs) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ValidAliasExpansionFunc returns a function that will check if the given string
|
||||
// is a valid alias expansion. An expansion is valid if:
|
||||
// - it is a shell expansion,
|
||||
// - it is a non-shell expansion that corresponds to an existing command, extension, or alias.
|
||||
func ValidAliasExpansionFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(expansion string) bool {
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
return true
|
||||
}
|
||||
|
||||
split, err := shlex.Split(expansion)
|
||||
if err != nil || len(split) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, _ = rootCmd.Find(split)
|
||||
return cmd != rootCmd
|
||||
}
|
||||
}
|
||||
54
pkg/cmd/alias/shared/validations_test.go
Normal file
54
pkg/cmd/alias/shared/validations_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidAliasNameFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ValidAliasNameFunc(cmd)
|
||||
|
||||
assert.False(t, f("pr"))
|
||||
assert.False(t, f("pr checkout"))
|
||||
assert.False(t, f("issue"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("ps"))
|
||||
assert.True(t, f("checkout"))
|
||||
assert.True(t, f("issue erase"))
|
||||
assert.True(t, f("pr erase"))
|
||||
assert.True(t, f("pr checkout branch"))
|
||||
}
|
||||
|
||||
func TestValidAliasExpansionFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ValidAliasExpansionFunc(cmd)
|
||||
|
||||
assert.False(t, f("ps"))
|
||||
assert.False(t, f("checkout"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("!git branch --show-current"))
|
||||
assert.True(t, f("pr"))
|
||||
assert.True(t, f("pr checkout"))
|
||||
assert.True(t, f("issue"))
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -23,8 +24,8 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/jsoncolor"
|
||||
"github.com/cli/go-gh/pkg/jq"
|
||||
"github.com/cli/go-gh/pkg/template"
|
||||
"github.com/cli/go-gh/v2/pkg/jq"
|
||||
"github.com/cli/go-gh/v2/pkg/template"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -321,6 +322,7 @@ func apiRun(opts *ApiOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
isFirstPage := true
|
||||
hasNextPage := true
|
||||
for hasNextPage {
|
||||
resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders)
|
||||
|
|
@ -328,10 +330,16 @@ func apiRun(opts *ApiOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
endCursor, err := processResponse(resp, opts, bodyWriter, headersWriter, &tmpl)
|
||||
if !isGraphQL {
|
||||
requestPath, hasNextPage = findNextPage(resp)
|
||||
requestBody = nil // prevent repeating GET parameters
|
||||
}
|
||||
|
||||
endCursor, err := processResponse(resp, opts, bodyWriter, headersWriter, tmpl, isFirstPage, !hasNextPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFirstPage = false
|
||||
|
||||
if !opts.Paginate {
|
||||
break
|
||||
|
|
@ -342,9 +350,6 @@ func apiRun(opts *ApiOptions) error {
|
|||
if hasNextPage {
|
||||
params["endCursor"] = endCursor
|
||||
}
|
||||
} else {
|
||||
requestPath, hasNextPage = findNextPage(resp)
|
||||
requestBody = nil // prevent repeating GET parameters
|
||||
}
|
||||
|
||||
if hasNextPage && opts.ShowResponseHeaders {
|
||||
|
|
@ -355,7 +360,7 @@ func apiRun(opts *ApiOptions) error {
|
|||
return tmpl.Flush()
|
||||
}
|
||||
|
||||
func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersWriter io.Writer, template *template.Template) (endCursor string, err error) {
|
||||
func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersWriter io.Writer, template *template.Template, isFirstPage, isLastPage bool) (endCursor string, err error) {
|
||||
if opts.ShowResponseHeaders {
|
||||
fmt.Fprintln(headersWriter, resp.Proto, resp.Status)
|
||||
printHeaders(headersWriter, resp.Header, opts.IO.ColorEnabled())
|
||||
|
|
@ -387,7 +392,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
|
|||
|
||||
if opts.FilterOutput != "" && serverError == "" {
|
||||
// TODO: reuse parsed query across pagination invocations
|
||||
err = jq.Evaluate(responseBody, bodyWriter, opts.FilterOutput)
|
||||
indent := ""
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
indent = " "
|
||||
}
|
||||
err = jq.EvaluateFormatted(responseBody, bodyWriter, opts.FilterOutput, indent, opts.IO.ColorEnabled())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -399,6 +408,13 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
|
|||
} else if isJSON && opts.IO.ColorEnabled() {
|
||||
err = jsoncolor.Write(bodyWriter, responseBody, " ")
|
||||
} else {
|
||||
if isJSON && opts.Paginate && !isGraphQLPaginate && !opts.ShowResponseHeaders {
|
||||
responseBody = &paginatedArrayReader{
|
||||
Reader: responseBody,
|
||||
isFirstPage: isFirstPage,
|
||||
isLastPage: isLastPage,
|
||||
}
|
||||
}
|
||||
_, err = io.Copy(bodyWriter, responseBody)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
@ -454,6 +470,11 @@ func fillPlaceholders(value string, opts *ApiOptions) (string, error) {
|
|||
err = e
|
||||
}
|
||||
case "branch":
|
||||
if os.Getenv("GH_REPO") != "" {
|
||||
err = errors.New("unable to determine an appropriate value for the 'branch' placeholder")
|
||||
return m
|
||||
}
|
||||
|
||||
if branch, e := opts.Branch(); e == nil {
|
||||
return branch
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import (
|
|||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/go-gh/pkg/template"
|
||||
"github.com/cli/go-gh/v2/pkg/template"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -378,6 +378,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err error
|
||||
stdout string
|
||||
stderr string
|
||||
isatty bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
|
|
@ -388,6 +389,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: `bam!`,
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "show response headers",
|
||||
|
|
@ -404,6 +406,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\nbody",
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "success 204",
|
||||
|
|
@ -414,6 +417,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: ``,
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "REST error",
|
||||
|
|
@ -425,6 +429,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `{"message": "THIS IS FINE"}`,
|
||||
stderr: "gh: THIS IS FINE (HTTP 400)\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "REST string errors",
|
||||
|
|
@ -436,6 +441,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `{"errors": ["ALSO", "FINE"]}`,
|
||||
stderr: "gh: ALSO\nFINE\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "GraphQL error",
|
||||
|
|
@ -450,6 +456,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `{"errors": [{"message":"AGAIN"}, {"message":"FINE"}]}`,
|
||||
stderr: "gh: AGAIN\nFINE\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "failure",
|
||||
|
|
@ -460,6 +467,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `gateway timeout`,
|
||||
stderr: "gh: HTTP 502\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "silent",
|
||||
|
|
@ -473,6 +481,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: ``,
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "show response headers even when silent",
|
||||
|
|
@ -490,6 +499,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: "HTTP/1.1 200 Okey-dokey\nContent-Type: text/plain\r\n\r\n",
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "output template",
|
||||
|
|
@ -504,6 +514,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: "not a cat",
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "output template when REST error",
|
||||
|
|
@ -518,6 +529,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `{"message": "THIS IS FINE"}`,
|
||||
stderr: "gh: THIS IS FINE (HTTP 400)\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "jq filter",
|
||||
|
|
@ -532,6 +544,7 @@ func Test_apiRun(t *testing.T) {
|
|||
err: nil,
|
||||
stdout: "Mona\nHubot\n",
|
||||
stderr: ``,
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "jq filter when REST error",
|
||||
|
|
@ -546,12 +559,29 @@ func Test_apiRun(t *testing.T) {
|
|||
err: cmdutil.SilentError,
|
||||
stdout: `{"message": "THIS IS FINE"}`,
|
||||
stderr: "gh: THIS IS FINE (HTTP 400)\n",
|
||||
isatty: false,
|
||||
},
|
||||
{
|
||||
name: "jq filter outputting JSON to a TTY",
|
||||
options: ApiOptions{
|
||||
FilterOutput: `.`,
|
||||
},
|
||||
httpResponse: &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"name":"Mona"},{"name":"Hubot"}]`)),
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
},
|
||||
err: nil,
|
||||
stdout: "[\n {\n \"name\": \"Mona\"\n },\n {\n \"name\": \"Hubot\"\n }\n]\n",
|
||||
stderr: ``,
|
||||
isatty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.isatty)
|
||||
|
||||
tt.options.IO = ios
|
||||
tt.options.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil }
|
||||
|
|
@ -638,6 +668,78 @@ func Test_apiRun_paginationREST(t *testing.T) {
|
|||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=3", responses[2].Request.URL.String())
|
||||
}
|
||||
|
||||
func Test_apiRun_arrayPaginationREST(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
requestCount := 0
|
||||
responses := []*http.Response{
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":1},{"item":2}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=2>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":3},{"item":4}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=3>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":5}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=4>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
options := ApiOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
|
||||
resp := responses[requestCount]
|
||||
resp.Request = req
|
||||
requestCount++
|
||||
return resp, nil
|
||||
}
|
||||
return &http.Client{Transport: tr}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: true,
|
||||
RequestPath: "issues",
|
||||
Paginate: true,
|
||||
RawFields: []string{"per_page=50", "page=1"},
|
||||
}
|
||||
|
||||
err := apiRun(&options)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[{"item":1},{"item":2},{"item":3},{"item":4},{"item":5} ]`, stdout.String(), "stdout")
|
||||
assert.Equal(t, "", stderr.String(), "stderr")
|
||||
|
||||
assert.Equal(t, "https://api.github.com/issues?page=1&per_page=50", responses[0].Request.URL.String())
|
||||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=2", responses[1].Request.URL.String())
|
||||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=3", responses[2].Request.URL.String())
|
||||
}
|
||||
|
||||
func Test_apiRun_paginationGraphQL(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -994,10 +1096,11 @@ func Test_fillPlaceholders(t *testing.T) {
|
|||
opts *ApiOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
name string
|
||||
args args
|
||||
repoOverride bool
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
|
|
@ -1134,9 +1237,26 @@ func Test_fillPlaceholders(t *testing.T) {
|
|||
want: "{}{ownership}/{repository}",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "branch can't be filled when GH_REPO is set",
|
||||
repoOverride: true,
|
||||
args: args{
|
||||
value: "repos/:owner/:repo/branches/:branch",
|
||||
opts: &ApiOptions{
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("hubot", "robot-uprising"), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "repos/hubot/robot-uprising/branches/:branch",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.repoOverride {
|
||||
t.Setenv("GH_REPO", "hubot/robot-uprising")
|
||||
}
|
||||
got, err := fillPlaceholders(tt.args.value, tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fillPlaceholders() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
|
@ -1206,7 +1326,7 @@ func Test_processResponse_template(t *testing.T) {
|
|||
tmpl := template.New(ios.Out, ios.TerminalWidth(), ios.ColorEnabled())
|
||||
err := tmpl.Parse(opts.Template)
|
||||
require.NoError(t, err)
|
||||
_, err = processResponse(&resp, &opts, ios.Out, io.Discard, &tmpl)
|
||||
_, err = processResponse(&resp, &opts, ios.Out, io.Discard, tmpl, true, true)
|
||||
require.NoError(t, err)
|
||||
err = tmpl.Flush()
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func httpRequest(client *http.Client, hostname string, method string, p string,
|
|||
return nil, fmt.Errorf("unrecognized parameters type: %v", params)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, requestURL, body)
|
||||
req, err := http.NewRequest(strings.ToUpper(method), requestURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -103,21 +103,8 @@ func addQuery(path string, params map[string]interface{}) string {
|
|||
}
|
||||
|
||||
query := url.Values{}
|
||||
for key, value := range params {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
query.Add(key, v)
|
||||
case []byte:
|
||||
query.Add(key, string(v))
|
||||
case nil:
|
||||
query.Add(key, "")
|
||||
case int:
|
||||
query.Add(key, fmt.Sprintf("%d", v))
|
||||
case bool:
|
||||
query.Add(key, fmt.Sprintf("%v", v))
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %v", v))
|
||||
}
|
||||
if err := addQueryParam(query, "", params); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sep := "?"
|
||||
|
|
@ -126,3 +113,34 @@ func addQuery(path string, params map[string]interface{}) string {
|
|||
}
|
||||
return path + sep + query.Encode()
|
||||
}
|
||||
|
||||
func addQueryParam(query url.Values, key string, value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
query.Add(key, v)
|
||||
case []byte:
|
||||
query.Add(key, string(v))
|
||||
case nil:
|
||||
query.Add(key, "")
|
||||
case int:
|
||||
query.Add(key, fmt.Sprintf("%d", v))
|
||||
case bool:
|
||||
query.Add(key, fmt.Sprintf("%v", v))
|
||||
case map[string]interface{}:
|
||||
for subkey, value := range v {
|
||||
// support for nested subkeys can be added here if that is ever necessary
|
||||
if err := addQueryParam(query, subkey, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, entry := range v {
|
||||
if err := addQueryParam(query, key+"[]", entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown type %v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,24 @@ func Test_httpRequest(t *testing.T) {
|
|||
headers: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lowercase HTTP method",
|
||||
args: args{
|
||||
client: &httpClient,
|
||||
host: "github.com",
|
||||
method: "get",
|
||||
p: "repos/octocat/spoon-knife",
|
||||
params: nil,
|
||||
headers: []string{},
|
||||
},
|
||||
wantErr: false,
|
||||
want: expects{
|
||||
method: "GET",
|
||||
u: "https://api.github.com/repos/octocat/spoon-knife",
|
||||
body: "",
|
||||
headers: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET with leading slash",
|
||||
args: args{
|
||||
|
|
@ -322,6 +340,14 @@ func Test_addQuery(t *testing.T) {
|
|||
},
|
||||
want: "?a=hello",
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
args: args{
|
||||
path: "",
|
||||
params: map[string]interface{}{"a": []interface{}{"hello", "world"}},
|
||||
},
|
||||
want: "?a%5B%5D=hello&a%5B%5D=world",
|
||||
},
|
||||
{
|
||||
name: "append",
|
||||
args: args{
|
||||
|
|
|
|||
|
|
@ -106,3 +106,43 @@ func addPerPage(p string, perPage int, params map[string]interface{}) string {
|
|||
|
||||
return fmt.Sprintf("%s%sper_page=%d", p, sep, perPage)
|
||||
}
|
||||
|
||||
// paginatedArrayReader wraps a Reader to omit the opening and/or the closing square bracket of a
|
||||
// JSON array in order to apply pagination context between multiple API requests.
|
||||
type paginatedArrayReader struct {
|
||||
io.Reader
|
||||
isFirstPage bool
|
||||
isLastPage bool
|
||||
|
||||
isSubsequentRead bool
|
||||
cachedByte byte
|
||||
}
|
||||
|
||||
func (r *paginatedArrayReader) Read(p []byte) (int, error) {
|
||||
var n int
|
||||
var err error
|
||||
if r.cachedByte != 0 && len(p) > 0 {
|
||||
p[0] = r.cachedByte
|
||||
n, err = r.Reader.Read(p[1:])
|
||||
n += 1
|
||||
r.cachedByte = 0
|
||||
} else {
|
||||
n, err = r.Reader.Read(p)
|
||||
}
|
||||
if !r.isSubsequentRead && !r.isFirstPage && n > 0 && p[0] == '[' {
|
||||
if n > 1 && p[1] == ']' {
|
||||
// empty array case
|
||||
p[0] = ' '
|
||||
} else {
|
||||
// avoid starting a new array and continue with a comma instead
|
||||
p[0] = ','
|
||||
}
|
||||
}
|
||||
if !r.isLastPage && n > 0 && p[n-1] == ']' {
|
||||
// avoid closing off an array in case we determine we are at EOF
|
||||
r.cachedByte = p[n-1]
|
||||
n -= 1
|
||||
}
|
||||
r.isSubsequentRead = true
|
||||
return n, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
ghAuth "github.com/cli/go-gh/pkg/auth"
|
||||
ghAuth "github.com/cli/go-gh/v2/pkg/auth"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -151,6 +151,11 @@ func loginRun(opts *LoginOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
// The go-gh Config object currently does not support case-insensitive lookups for host names,
|
||||
// so normalize the host name case here before performing any lookups with it or persisting it.
|
||||
// https://github.com/cli/go-gh/pull/105
|
||||
hostname = strings.ToLower(hostname)
|
||||
|
||||
if src, writeable := shared.AuthTokenWriteable(authCfg, hostname); !writeable {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", src)
|
||||
fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/set"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -24,9 +25,11 @@ type RefreshOptions struct {
|
|||
|
||||
MainExecutable string
|
||||
|
||||
Hostname string
|
||||
Scopes []string
|
||||
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
|
||||
Hostname string
|
||||
Scopes []string
|
||||
RemoveScopes []string
|
||||
ResetScopes bool
|
||||
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
|
||||
|
||||
Interactive bool
|
||||
InsecureStorage bool
|
||||
|
|
@ -62,16 +65,25 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
|
|||
your gh credentials to have. If no scopes are provided, the command
|
||||
maintains previously added scopes.
|
||||
|
||||
The command can only add additional scopes, but not remove previously
|
||||
added ones. To reset scopes to the default minimum set of scopes, you
|
||||
will need to create new credentials using the auth login command.
|
||||
The --remove-scopes flag accepts a comma separated list of scopes you
|
||||
want to remove from your gh credentials. Scope removal is idempotent.
|
||||
The minimum set of scopes ("repo", "read:org" and "gist") cannot be removed.
|
||||
|
||||
The --reset-scopes flag resets the scopes for your gh credentials to
|
||||
the default set of scopes for your auth flow.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
$ gh auth refresh --scopes write:org,read:public_key
|
||||
# => open a browser to add write:org and read:public_key scopes for use with gh api
|
||||
# => open a browser to add write:org and read:public_key scopes
|
||||
|
||||
$ gh auth refresh
|
||||
# => open a browser to ensure your authentication credentials have the correct minimum scopes
|
||||
|
||||
$ gh auth refresh --remove-scopes delete_repo
|
||||
# => open a browser to idempotently remove the delete_repo scope
|
||||
|
||||
$ gh auth refresh --reset-scopes
|
||||
# => open a browser to re-authenticate with the default minimum scopes
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Interactive = opts.IO.CanPrompt()
|
||||
|
|
@ -90,6 +102,8 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
|
|||
|
||||
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication")
|
||||
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
|
||||
cmd.Flags().StringSliceVarP(&opts.RemoveScopes, "remove-scopes", "r", nil, "Authentication scopes to remove from gh")
|
||||
cmd.Flags().BoolVar(&opts.ResetScopes, "reset-scopes", false, "Reset authentication scopes to the default minimum set of scopes")
|
||||
// secure storage became the default on 2023/4/04; this flag is left as a no-op for backwards compatibility
|
||||
var secureStorage bool
|
||||
cmd.Flags().BoolVar(&secureStorage, "secure-storage", false, "Save authentication credentials in secure credential store")
|
||||
|
|
@ -143,13 +157,16 @@ func refreshRun(opts *RefreshOptions) error {
|
|||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
var additionalScopes []string
|
||||
if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
|
||||
if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
|
||||
for _, s := range strings.Split(oldScopes, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
additionalScopes = append(additionalScopes, s)
|
||||
additionalScopes := set.NewStringSet()
|
||||
|
||||
if !opts.ResetScopes {
|
||||
if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
|
||||
if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
|
||||
for _, s := range strings.Split(oldScopes, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
additionalScopes.Add(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,10 +182,14 @@ func refreshRun(opts *RefreshOptions) error {
|
|||
if err := credentialFlow.Prompt(hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
|
||||
additionalScopes.AddValues(credentialFlow.Scopes())
|
||||
}
|
||||
|
||||
if err := opts.AuthFlow(authCfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive, !opts.InsecureStorage); err != nil {
|
||||
additionalScopes.AddValues(opts.Scopes)
|
||||
|
||||
additionalScopes.RemoveValues(opts.RemoveScopes)
|
||||
|
||||
if err := opts.AuthFlow(authCfg, opts.IO, hostname, additionalScopes.ToSlice(), opts.Interactive, !opts.InsecureStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TODO prompt cfg test
|
||||
|
||||
func Test_NewCmdRefresh(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -99,6 +97,38 @@ func Test_NewCmdRefresh(t *testing.T) {
|
|||
InsecureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reset scopes",
|
||||
tty: true,
|
||||
cli: "--reset-scopes",
|
||||
wants: RefreshOptions{
|
||||
ResetScopes: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove scope",
|
||||
tty: true,
|
||||
cli: "--remove-scopes read:public_key",
|
||||
wants: RefreshOptions{
|
||||
RemoveScopes: []string{"read:public_key"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove multiple scopes",
|
||||
tty: true,
|
||||
cli: "--remove-scopes workflow,read:public_key",
|
||||
wants: RefreshOptions{
|
||||
RemoveScopes: []string{"workflow", "read:public_key"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove scope shorthand",
|
||||
tty: true,
|
||||
cli: "-r read:public_key",
|
||||
wants: RefreshOptions{
|
||||
RemoveScopes: []string{"read:public_key"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -185,7 +215,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "obed.morton",
|
||||
scopes: nil,
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -199,7 +229,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: nil,
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -219,7 +249,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: nil,
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -248,7 +278,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{"repo:invite", "public_key:read", "delete_repo", "codespace"},
|
||||
scopes: []string{"delete_repo", "codespace", "repo:invite", "public_key:read"},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -262,7 +292,7 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "obed.morton",
|
||||
scopes: nil,
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -277,7 +307,101 @@ func Test_refreshRun(t *testing.T) {
|
|||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "obed.morton",
|
||||
scopes: nil,
|
||||
scopes: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reset scopes",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
oldScopes: "delete_repo, codespace",
|
||||
opts: &RefreshOptions{
|
||||
Hostname: "github.com",
|
||||
ResetScopes: true,
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reset scopes and add some scopes",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
oldScopes: "repo:invite, delete_repo, codespace",
|
||||
opts: &RefreshOptions{
|
||||
Scopes: []string{"public_key:read", "workflow"},
|
||||
ResetScopes: true,
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{"public_key:read", "workflow"},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove scopes",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
oldScopes: "delete_repo, codespace, repo:invite, public_key:read",
|
||||
opts: &RefreshOptions{
|
||||
Hostname: "github.com",
|
||||
RemoveScopes: []string{"delete_repo", "repo:invite"},
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{"codespace", "public_key:read"},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove scope but no old scope",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
opts: &RefreshOptions{
|
||||
Hostname: "github.com",
|
||||
RemoveScopes: []string{"delete_repo"},
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove and add scopes at the same time",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
oldScopes: "repo:invite, delete_repo, codespace",
|
||||
opts: &RefreshOptions{
|
||||
Scopes: []string{"repo:invite", "public_key:read", "workflow"},
|
||||
RemoveScopes: []string{"codespace", "repo:invite", "workflow"},
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{"delete_repo", "public_key:read"},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove scopes that don't exist",
|
||||
cfgHosts: []string{
|
||||
"github.com",
|
||||
},
|
||||
oldScopes: "repo:invite, delete_repo, codespace",
|
||||
opts: &RefreshOptions{
|
||||
RemoveScopes: []string{"codespace", "repo:invite", "public_key:read"},
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "github.com",
|
||||
scopes: []string{"delete_repo"},
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func statusRun(opts *StatusOptions) error {
|
|||
// TODO check tty
|
||||
|
||||
stderr := opts.IO.ErrOut
|
||||
|
||||
stdout := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
statusInfo := map[string][]string{}
|
||||
|
|
@ -166,13 +166,22 @@ func statusRun(opts *StatusOptions) error {
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
if prevEntry {
|
||||
if prevEntry && failed {
|
||||
fmt.Fprint(stderr, "\n")
|
||||
} else if prevEntry && !failed {
|
||||
fmt.Fprint(stdout, "\n")
|
||||
}
|
||||
prevEntry = true
|
||||
fmt.Fprintf(stderr, "%s\n", cs.Bold(hostname))
|
||||
for _, line := range lines {
|
||||
fmt.Fprintf(stderr, " %s\n", line)
|
||||
if failed {
|
||||
fmt.Fprintf(stderr, "%s\n", cs.Bold(hostname))
|
||||
for _, line := range lines {
|
||||
fmt.Fprintf(stderr, " %s\n", line)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(stdout, "%s\n", cs.Bold(hostname))
|
||||
for _, line := range lines {
|
||||
fmt.Fprintf(stdout, " %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,12 +76,13 @@ func Test_statusRun(t *testing.T) {
|
|||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *StatusOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
wantErr string
|
||||
wantOut string
|
||||
name string
|
||||
opts *StatusOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
wantErr string
|
||||
wantOut string
|
||||
wantErrOut string
|
||||
}{
|
||||
{
|
||||
name: "hostname set",
|
||||
|
|
@ -126,7 +127,7 @@ func Test_statusRun(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
},
|
||||
wantErr: "SilentError",
|
||||
wantOut: heredoc.Doc(`
|
||||
wantErrOut: heredoc.Doc(`
|
||||
joel.miller
|
||||
X joel.miller: the token in GH_CONFIG_DIR/hosts.yml is missing required scope 'read:org'
|
||||
- To request missing scopes, run: gh auth refresh -h joel.miller
|
||||
|
|
@ -156,7 +157,7 @@ func Test_statusRun(t *testing.T) {
|
|||
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
|
||||
},
|
||||
wantErr: "SilentError",
|
||||
wantOut: heredoc.Doc(`
|
||||
wantErrOut: heredoc.Doc(`
|
||||
joel.miller
|
||||
X joel.miller: authentication failed
|
||||
- The joel.miller token in GH_CONFIG_DIR/hosts.yml is no longer valid.
|
||||
|
|
@ -298,9 +299,9 @@ func Test_statusRun(t *testing.T) {
|
|||
cfgStubs: func(c *config.ConfigMock) {
|
||||
c.Set("github.com", "oauth_token", "abc123")
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
wantErr: "SilentError",
|
||||
wantOut: "Hostname \"github.example.com\" not found among authenticated GitHub hosts\n",
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
wantErr: "SilentError",
|
||||
wantErrOut: "Hostname \"github.example.com\" not found among authenticated GitHub hosts\n",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -310,13 +311,12 @@ func Test_statusRun(t *testing.T) {
|
|||
tt.opts = &StatusOptions{}
|
||||
}
|
||||
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
tt.opts.IO = ios
|
||||
|
||||
cfg := config.NewFromString("")
|
||||
if tt.cfgStubs != nil {
|
||||
tt.cfgStubs(cfg)
|
||||
|
|
@ -340,8 +340,10 @@ func Test_statusRun(t *testing.T) {
|
|||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
output := strings.ReplaceAll(stdout.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
|
||||
errorOutput := strings.ReplaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
|
||||
|
||||
output := strings.ReplaceAll(stderr.String(), config.ConfigDir()+string(filepath.Separator), "GH_CONFIG_DIR/")
|
||||
assert.Equal(t, tt.wantErrOut, errorOutput)
|
||||
assert.Equal(t, tt.wantOut, output)
|
||||
|
||||
mainBuf := bytes.Buffer{}
|
||||
|
|
|
|||
|
|
@ -163,14 +163,12 @@ func runBrowse(opts *BrowseOptions) error {
|
|||
return fmt.Errorf("unable to determine base repository: %w", err)
|
||||
}
|
||||
|
||||
if opts.Commit != "" {
|
||||
if opts.Commit == emptyCommitFlag {
|
||||
commit, err := opts.GitClient.LastCommit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Commit = commit.Sha
|
||||
if opts.Commit != "" && opts.Commit == emptyCommitFlag {
|
||||
commit, err := opts.GitClient.LastCommit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Commit = commit.Sha
|
||||
}
|
||||
|
||||
section, err := parseSection(baseRepo, opts)
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ func Test_runBrowse(t *testing.T) {
|
|||
opts: BrowseOptions{
|
||||
SelectorArg: "chocolate-pecan-pie.txt",
|
||||
},
|
||||
baseRepo: ghrepo.New("andrewhsu", "recipies"),
|
||||
baseRepo: ghrepo.New("andrewhsu", "recipes"),
|
||||
defaultBranch: "",
|
||||
wantsErr: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ type CodespaceSelector struct {
|
|||
|
||||
repoName string
|
||||
codespaceName string
|
||||
repoOwner string
|
||||
}
|
||||
|
||||
var errNoFilteredCodespaces = errors.New("you have no codespaces meeting the filter criteria")
|
||||
|
|
@ -25,8 +26,10 @@ func AddCodespaceSelector(cmd *cobra.Command, api apiClient) *CodespaceSelector
|
|||
|
||||
cmd.PersistentFlags().StringVarP(&cs.codespaceName, "codespace", "c", "", "Name of the codespace")
|
||||
cmd.PersistentFlags().StringVarP(&cs.repoName, "repo", "R", "", "Filter codespace selection by repository name (user/repo)")
|
||||
cmd.PersistentFlags().StringVar(&cs.repoOwner, "repo-owner", "", "Filter codespace selection by repository owner (username or org)")
|
||||
|
||||
cmd.MarkFlagsMutuallyExclusive("codespace", "repo")
|
||||
cmd.MarkFlagsMutuallyExclusive("codespace", "repo-owner")
|
||||
|
||||
return cs
|
||||
}
|
||||
|
|
@ -102,6 +105,10 @@ func (cs *CodespaceSelector) fetchCodespaces(ctx context.Context) (codespaces []
|
|||
codespaces = filteredCodespaces
|
||||
}
|
||||
|
||||
if cs.repoOwner != "" {
|
||||
codespaces = filterCodespacesByRepoOwner(codespaces, cs.repoOwner)
|
||||
}
|
||||
|
||||
if len(codespaces) == 0 {
|
||||
return nil, errNoFilteredCodespaces
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,68 +48,101 @@ func TestSelectNameWithCodespaceName(t *testing.T) {
|
|||
|
||||
func TestFetchCodespaces(t *testing.T) {
|
||||
var (
|
||||
repoA1 = &api.Codespace{Name: "1", Repository: api.Repository{FullName: "mock/A"}}
|
||||
repoA2 = &api.Codespace{Name: "2", Repository: api.Repository{FullName: "mock/A"}}
|
||||
octocatOwner = api.RepositoryOwner{Login: "octocat"}
|
||||
cliOwner = api.RepositoryOwner{Login: "cli"}
|
||||
octocatA = &api.Codespace{
|
||||
Name: "1",
|
||||
Repository: api.Repository{FullName: "octocat/A", Owner: octocatOwner},
|
||||
}
|
||||
|
||||
repoB1 = &api.Codespace{Name: "1", Repository: api.Repository{FullName: "mock/B"}}
|
||||
octocatA2 = &api.Codespace{
|
||||
Name: "2",
|
||||
Repository: api.Repository{FullName: "octocat/A", Owner: octocatOwner},
|
||||
}
|
||||
|
||||
cliA = &api.Codespace{
|
||||
Name: "3",
|
||||
Repository: api.Repository{FullName: "cli/A", Owner: cliOwner},
|
||||
}
|
||||
|
||||
octocatB = &api.Codespace{
|
||||
Name: "4",
|
||||
Repository: api.Repository{FullName: "octocat/B", Owner: octocatOwner},
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
tName string
|
||||
apiCodespaces []*api.Codespace
|
||||
codespaceName string
|
||||
repoName string
|
||||
repoOwner string
|
||||
wantCodespaces []*api.Codespace
|
||||
wantErr error
|
||||
}{
|
||||
// Empty case
|
||||
{
|
||||
"empty", nil, "", nil, errNoCodespaces,
|
||||
tName: "empty",
|
||||
apiCodespaces: nil,
|
||||
wantCodespaces: nil,
|
||||
wantErr: errNoCodespaces,
|
||||
},
|
||||
|
||||
// Tests with no filtering
|
||||
{
|
||||
"no filtering, single codespace",
|
||||
[]*api.Codespace{repoA1},
|
||||
"",
|
||||
[]*api.Codespace{repoA1},
|
||||
nil,
|
||||
tName: "no filtering, single codespaces",
|
||||
apiCodespaces: []*api.Codespace{octocatA},
|
||||
wantCodespaces: []*api.Codespace{octocatA},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
"no filtering, multiple codespaces",
|
||||
[]*api.Codespace{repoA1, repoA2, repoB1},
|
||||
"",
|
||||
[]*api.Codespace{repoA1, repoA2, repoB1},
|
||||
nil,
|
||||
tName: "no filtering, multiple codespace",
|
||||
apiCodespaces: []*api.Codespace{octocatA, cliA, octocatB},
|
||||
wantCodespaces: []*api.Codespace{octocatA, cliA, octocatB},
|
||||
},
|
||||
|
||||
// Test repo filtering
|
||||
{
|
||||
"repo filtering, single codespace",
|
||||
[]*api.Codespace{repoA1},
|
||||
"mock/A",
|
||||
[]*api.Codespace{repoA1},
|
||||
nil,
|
||||
tName: "repo name filtering, single codespace",
|
||||
apiCodespaces: []*api.Codespace{octocatA},
|
||||
repoName: "octocat/A",
|
||||
wantCodespaces: []*api.Codespace{octocatA},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
"repo filtering, multiple codespaces",
|
||||
[]*api.Codespace{repoA1, repoA2, repoB1},
|
||||
"mock/A",
|
||||
[]*api.Codespace{repoA1, repoA2},
|
||||
nil,
|
||||
tName: "repo name filtering, multiple codespace",
|
||||
apiCodespaces: []*api.Codespace{octocatA, octocatA2, cliA, octocatB},
|
||||
repoName: "octocat/A",
|
||||
wantCodespaces: []*api.Codespace{octocatA, octocatA2},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
"repo filtering, multiple codespaces 2",
|
||||
[]*api.Codespace{repoA1, repoA2, repoB1},
|
||||
"mock/B",
|
||||
[]*api.Codespace{repoB1},
|
||||
nil,
|
||||
tName: "repo name filtering, multiple codespace 2",
|
||||
apiCodespaces: []*api.Codespace{octocatA, cliA, octocatB},
|
||||
repoName: "octocat/B",
|
||||
wantCodespaces: []*api.Codespace{octocatB},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
"repo filtering, no matches",
|
||||
[]*api.Codespace{repoA1, repoA2, repoB1},
|
||||
"mock/C",
|
||||
nil,
|
||||
errNoFilteredCodespaces,
|
||||
tName: "repo name filtering, no matches",
|
||||
apiCodespaces: []*api.Codespace{octocatA, cliA, octocatB},
|
||||
repoName: "Unknown/unknown",
|
||||
wantCodespaces: nil,
|
||||
wantErr: errNoFilteredCodespaces,
|
||||
},
|
||||
{
|
||||
tName: "repo filtering, match with repo owner",
|
||||
apiCodespaces: []*api.Codespace{octocatA, octocatA2, cliA, octocatB},
|
||||
repoOwner: "octocat",
|
||||
wantCodespaces: []*api.Codespace{octocatA, octocatA2, octocatB},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
tName: "repo filtering, no match with repo owner",
|
||||
apiCodespaces: []*api.Codespace{octocatA, cliA, octocatB},
|
||||
repoOwner: "unknown",
|
||||
wantCodespaces: []*api.Codespace{},
|
||||
wantErr: errNoFilteredCodespaces,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +154,11 @@ func TestFetchCodespaces(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
cs := &CodespaceSelector{api: api, repoName: tt.repoName}
|
||||
cs := &CodespaceSelector{
|
||||
api: api,
|
||||
repoName: tt.repoName,
|
||||
repoOwner: tt.repoOwner,
|
||||
}
|
||||
|
||||
codespaces, err := cs.fetchCodespaces(context.Background())
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
|
|
@ -266,3 +267,14 @@ func addDeprecatedRepoShorthand(cmd *cobra.Command, target *string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterCodespacesByRepoOwner filters a list of codespaces by the owner of the repository.
|
||||
func filterCodespacesByRepoOwner(codespaces []*api.Codespace, repoOwner string) []*api.Codespace {
|
||||
filtered := make([]*api.Codespace, 0, len(codespaces))
|
||||
for _, c := range codespaces {
|
||||
if strings.EqualFold(c.Repository.Owner.Login, repoOwner) {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
|
@ -23,6 +24,7 @@ type deleteOptions struct {
|
|||
keepDays uint16
|
||||
orgName string
|
||||
userName string
|
||||
repoOwner string
|
||||
|
||||
isInteractive bool
|
||||
now func() time.Time
|
||||
|
|
@ -60,10 +62,12 @@ func newDeleteCmd(app *App) *cobra.Command {
|
|||
// After the admin subcommand is added (see https://github.com/cli/cli/pull/6944#issuecomment-1419553639) we can revisit this.
|
||||
opts.codespaceName = selector.codespaceName
|
||||
opts.repoFilter = selector.repoName
|
||||
opts.repoOwner = selector.repoOwner
|
||||
|
||||
if opts.deleteAll && opts.repoFilter != "" {
|
||||
return cmdutil.FlagErrorf("both `--all` and `--repo` is not supported")
|
||||
}
|
||||
|
||||
if opts.orgName != "" && opts.codespaceName != "" && opts.userName == "" {
|
||||
return cmdutil.FlagErrorf("using `--org` with `--codespace` requires `--user`")
|
||||
}
|
||||
|
|
@ -99,6 +103,9 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
userName = currentUser.Login
|
||||
}
|
||||
codespaces, fetchErr = a.apiClient.ListCodespaces(ctx, api.ListCodespacesOptions{OrgName: opts.orgName, UserName: userName})
|
||||
if opts.repoOwner != "" {
|
||||
codespaces = filterCodespacesByRepoOwner(codespaces, opts.repoOwner)
|
||||
}
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -139,6 +146,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
if opts.repoFilter != "" && !strings.EqualFold(c.Repository.FullName, opts.repoFilter) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.keepDays > 0 {
|
||||
t, err := time.Parse(time.RFC3339, c.LastUsedAt)
|
||||
if err != nil {
|
||||
|
|
@ -169,7 +177,8 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
progressLabel = "Deleting codespaces"
|
||||
}
|
||||
|
||||
return a.RunWithProgress(progressLabel, func() error {
|
||||
var deletedCodespaces uint32
|
||||
err = a.RunWithProgress(progressLabel, func() error {
|
||||
var g errgroup.Group
|
||||
for _, c := range codespacesToDelete {
|
||||
codespaceName := c.Name
|
||||
|
|
@ -178,15 +187,23 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
|
|||
a.errLogger.Printf("error deleting codespace %q: %v\n", codespaceName, err)
|
||||
return err
|
||||
}
|
||||
atomic.AddUint32(&deletedCodespaces, 1)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return errors.New("some codespaces failed to delete")
|
||||
return fmt.Errorf("%d codespace(s) failed to delete", len(codespacesToDelete)-int(deletedCodespaces))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if a.io.IsStdoutTTY() && deletedCodespaces > 0 {
|
||||
successMsg := fmt.Sprintf("%d codespace(s) deleted successfully\n", deletedCodespaces)
|
||||
fmt.Fprint(a.io.ErrOut, successMsg)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func confirmDeletion(p prompter, apiCodespace *api.Codespace, isInteractive bool) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func TestDelete(t *testing.T) {
|
|||
codespaces []*api.Codespace
|
||||
confirms map[string]bool
|
||||
deleteErr error
|
||||
wantErr bool
|
||||
wantErr string
|
||||
wantDeleted []string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
|
|
@ -42,7 +42,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc"},
|
||||
wantStdout: "",
|
||||
wantStderr: "1 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "by repo",
|
||||
|
|
@ -70,7 +70,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"monalisa-spoonknife-123", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "",
|
||||
wantStderr: "2 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "unused",
|
||||
|
|
@ -93,7 +93,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "",
|
||||
wantStderr: "2 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "deletion failed",
|
||||
|
|
@ -109,12 +109,13 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
deleteErr: errors.New("aborted by test"),
|
||||
wantErr: true,
|
||||
wantErr: "2 codespace(s) failed to delete",
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-123"},
|
||||
wantStderr: heredoc.Doc(`
|
||||
error deleting codespace "hubot-robawt-abc": aborted by test
|
||||
error deleting codespace "monalisa-spoonknife-123": aborted by test
|
||||
`),
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "with confirm",
|
||||
|
|
@ -149,7 +150,7 @@ func TestDelete(t *testing.T) {
|
|||
"Codespace hubot-robawt-abc has unsaved changes. OK to delete?": true,
|
||||
},
|
||||
wantDeleted: []string{"hubot-robawt-abc", "monalisa-spoonknife-c4f3"},
|
||||
wantStdout: "",
|
||||
wantStderr: "2 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "deletion for org codespace by admin succeeds",
|
||||
|
|
@ -174,7 +175,7 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"monalisa-spoonknife-123"},
|
||||
wantStdout: "",
|
||||
wantStderr: "1 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "deletion for org codespace by admin fails for codespace not found",
|
||||
|
|
@ -200,7 +201,8 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
wantDeleted: []string{},
|
||||
wantStdout: "",
|
||||
wantErr: true,
|
||||
wantErr: "error fetching codespace information: " +
|
||||
"codespace not found for user johnDoe with name monalisa-spoonknife-123",
|
||||
},
|
||||
{
|
||||
name: "deletion for org codespace succeeds without username",
|
||||
|
|
@ -215,6 +217,45 @@ func TestDelete(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantDeleted: []string{"monalisa-spoonknife-123"},
|
||||
wantStderr: "1 codespace(s) deleted successfully\n",
|
||||
},
|
||||
{
|
||||
name: "by repo owner",
|
||||
opts: deleteOptions{
|
||||
deleteAll: true,
|
||||
repoOwner: "octocat",
|
||||
},
|
||||
codespaces: []*api.Codespace{
|
||||
{
|
||||
Name: "octocat-spoonknife-123",
|
||||
Repository: api.Repository{
|
||||
FullName: "octocat/Spoon-Knife",
|
||||
Owner: api.RepositoryOwner{
|
||||
Login: "octocat",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cli-robawt-abc",
|
||||
Repository: api.Repository{
|
||||
FullName: "cli/ROBAWT",
|
||||
Owner: api.RepositoryOwner{
|
||||
Login: "cli",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "octocat-spoonknife-c4f3",
|
||||
Repository: api.Repository{
|
||||
FullName: "octocat/Spoon-Knife",
|
||||
Owner: api.RepositoryOwner{
|
||||
Login: "octocat",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantDeleted: []string{"octocat-spoonknife-123", "octocat-spoonknife-c4f3"},
|
||||
wantStderr: "2 codespace(s) deleted successfully\n",
|
||||
wantStdout: "",
|
||||
},
|
||||
}
|
||||
|
|
@ -268,8 +309,8 @@ func TestDelete(t *testing.T) {
|
|||
ios.SetStdoutTTY(true)
|
||||
app := NewApp(ios, nil, apiMock, nil, nil)
|
||||
err := app.Delete(context.Background(), opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if (err != nil) && tt.wantErr != err.Error() {
|
||||
t.Errorf("delete() error = %v, wantErr = %v", err, tt.wantErr)
|
||||
}
|
||||
for _, listArgs := range apiMock.ListCodespacesCalls() {
|
||||
if listArgs.Opts.OrgName != "" && listArgs.Opts.UserName == "" {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func (a *App) Jupyter(ctx context.Context, selector *CodespaceSelector) (err err
|
|||
}
|
||||
|
||||
// Pass 0 to pick a random port
|
||||
listen, _, err := codespaces.ListenTCP(0)
|
||||
listen, _, err := codespaces.ListenTCP(0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func newListCmd(app *App) *cobra.Command {
|
|||
|
||||
listCmd.Flags().StringVarP(&opts.orgName, "org", "o", "", "The `login` handle of the organization to list codespaces for (admin-only)")
|
||||
listCmd.Flags().StringVarP(&opts.userName, "user", "u", "", "The `username` to list codespaces for (used with --org)")
|
||||
cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields)
|
||||
cmdutil.AddJSONFlags(listCmd, &exporter, api.ListCodespaceFields)
|
||||
|
||||
listCmd.Flags().BoolVarP(&opts.useWeb, "web", "w", false, "List codespaces in the web browser, cannot be used with --user or --org")
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (a *App) Logs(ctx context.Context, selector *CodespaceSelector, follow bool
|
|||
defer safeClose(session, &err)
|
||||
|
||||
// Ensure local port is listening before client (getPostCreateOutput) connects.
|
||||
listen, localPort, err := codespaces.ListenTCP(0)
|
||||
listen, localPort, err := codespaces.ListenTCP(0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ func getDevContainer(ctx context.Context, apiClient apiClient, codespace *api.Co
|
|||
|
||||
var container devContainer
|
||||
if err := json.Unmarshal(convertedJSON, &container); err != nil {
|
||||
ch <- devContainerResult{nil, fmt.Errorf("error unmarshaling: %w", err)}
|
||||
ch <- devContainerResult{nil, fmt.Errorf("error unmarshalling: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -379,7 +379,7 @@ func (a *App) ForwardPorts(ctx context.Context, selector *CodespaceSelector, por
|
|||
for _, pair := range portPairs {
|
||||
pair := pair
|
||||
group.Go(func() error {
|
||||
listen, _, err := codespaces.ListenTCP(pair.local)
|
||||
listen, _, err := codespaces.ListenTCP(pair.local, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func TestPendingOperationDisallowsListPorts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPendingOperationDisallowsUpdatePortVisability(t *testing.T) {
|
||||
func TestPendingOperationDisallowsUpdatePortVisibility(t *testing.T) {
|
||||
app := testingPortsApp()
|
||||
selector := &CodespaceSelector{api: app.apiClient, codespaceName: "disabledCodespace"}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func NewRootCmd(app *App) *cobra.Command {
|
|||
root.AddCommand(newDeleteCmd(app))
|
||||
root.AddCommand(newJupyterCmd(app))
|
||||
root.AddCommand(newListCmd(app))
|
||||
root.AddCommand(newViewCmd(app))
|
||||
root.AddCommand(newLogsCmd(app))
|
||||
root.AddCommand(newPortsCmd(app))
|
||||
root.AddCommand(newSSHCmd(app))
|
||||
|
|
|
|||
|
|
@ -36,14 +36,15 @@ const automaticPrivateKeyName = "codespaces.auto"
|
|||
var errKeyFileNotFound = errors.New("SSH key file does not exist")
|
||||
|
||||
type sshOptions struct {
|
||||
selector *CodespaceSelector
|
||||
profile string
|
||||
serverPort int
|
||||
debug bool
|
||||
debugFile string
|
||||
stdio bool
|
||||
config bool
|
||||
scpArgs []string // scp arguments, for 'cs cp' (nil for 'cs ssh')
|
||||
selector *CodespaceSelector
|
||||
profile string
|
||||
serverPort int
|
||||
printConnDetails bool
|
||||
debug bool
|
||||
debugFile string
|
||||
stdio bool
|
||||
config bool
|
||||
scpArgs []string // scp arguments, for 'cs cp' (nil for 'cs ssh')
|
||||
}
|
||||
|
||||
func newSSHCmd(app *App) *cobra.Command {
|
||||
|
|
@ -56,8 +57,14 @@ func newSSHCmd(app *App) *cobra.Command {
|
|||
The 'ssh' command is used to SSH into a codespace. In its simplest form, you can
|
||||
run 'gh cs ssh', select a codespace interactively, and connect.
|
||||
|
||||
By default, the 'ssh' command will create a public/private ssh key pair to
|
||||
authenticate with the codespace inside the ~/.ssh directory.
|
||||
The 'ssh' command will automatically create a public/private ssh key pair in the
|
||||
~/.ssh directory if you do not have an existing valid key pair. When selecting the
|
||||
key pair to use, the preferred order is:
|
||||
|
||||
1. Key specified by -i in <ssh-flags>
|
||||
2. Automatic key, if it already exists
|
||||
3. First valid key pair in ssh config (according to ssh -G)
|
||||
4. Automatic key, newly created
|
||||
|
||||
The 'ssh' command also supports deeper integration with OpenSSH using a '--config'
|
||||
option that generates per-codespace ssh configuration in OpenSSH format.
|
||||
|
|
@ -111,6 +118,9 @@ func newSSHCmd(app *App) *cobra.Command {
|
|||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cmd.Flag("server-port").Changed {
|
||||
opts.printConnDetails = true
|
||||
}
|
||||
if opts.config {
|
||||
return app.printOpenSSHConfig(cmd.Context(), opts)
|
||||
} else {
|
||||
|
|
@ -200,12 +210,11 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
}
|
||||
|
||||
localSSHServerPort := opts.serverPort
|
||||
usingCustomPort := localSSHServerPort != 0 // suppress log of command line in Shell
|
||||
|
||||
// Ensure local port is listening before client (Shell) connects.
|
||||
// Unless the user specifies a server port, localSSHServerPort is 0
|
||||
// and thus the client will pick a random port.
|
||||
listen, localSSHServerPort, err := codespaces.ListenTCP(localSSHServerPort)
|
||||
listen, localSSHServerPort, err := codespaces.ListenTCP(localSSHServerPort, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -229,7 +238,9 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
|
|||
// args is the correct variable to use here, we just use scpArgs as the check for which command to run
|
||||
err = codespaces.Copy(ctx, args, localSSHServerPort, connectDestination)
|
||||
} else {
|
||||
err = codespaces.Shell(ctx, a.errLogger, args, localSSHServerPort, connectDestination, usingCustomPort)
|
||||
err = codespaces.Shell(
|
||||
ctx, a.errLogger, args, localSSHServerPort, connectDestination, opts.printConnDetails,
|
||||
)
|
||||
}
|
||||
shellClosed <- err
|
||||
}()
|
||||
|
|
|
|||
133
pkg/cmd/codespace/view.go
Normal file
133
pkg/cmd/codespace/view.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
minutesInDay = 1440
|
||||
)
|
||||
|
||||
type viewOptions struct {
|
||||
selector *CodespaceSelector
|
||||
exporter cmdutil.Exporter
|
||||
}
|
||||
|
||||
func newViewCmd(app *App) *cobra.Command {
|
||||
opts := &viewOptions{}
|
||||
|
||||
viewCmd := &cobra.Command{
|
||||
Use: "view",
|
||||
Short: "View details about a codespace",
|
||||
Example: heredoc.Doc(`
|
||||
# select a codespace from a list of all codespaces you own
|
||||
$ gh cs view
|
||||
|
||||
# view the details of a specific codespace
|
||||
$ gh cs view -c codespace-name-12345
|
||||
|
||||
# view the list of all available fields for a codespace
|
||||
$ gh cs view --json
|
||||
|
||||
# view specific fields for a codespace
|
||||
$ gh cs view --json displayName,machineDisplayName,state
|
||||
`),
|
||||
Args: noArgsConstraint,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.ViewCodespace(cmd.Context(), opts)
|
||||
},
|
||||
}
|
||||
opts.selector = AddCodespaceSelector(viewCmd, app.apiClient)
|
||||
cmdutil.AddJSONFlags(viewCmd, &opts.exporter, api.ViewCodespaceFields)
|
||||
|
||||
return viewCmd
|
||||
}
|
||||
|
||||
func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions) error {
|
||||
// If we are in a codespace and a codespace name wasn't provided, show the details for the codespace we are connected to
|
||||
if (os.Getenv("CODESPACES") == "true") && opts.selector.codespaceName == "" {
|
||||
codespaceName := os.Getenv("CODESPACE_NAME")
|
||||
opts.selector.codespaceName = codespaceName
|
||||
}
|
||||
|
||||
selectedCodespace, err := opts.selector.Select(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.io.StartPager(); err != nil {
|
||||
a.errLogger.Printf("error starting pager: %v", err)
|
||||
}
|
||||
defer a.io.StopPager()
|
||||
|
||||
if opts.exporter != nil {
|
||||
return opts.exporter.Write(a.io, selectedCodespace)
|
||||
}
|
||||
|
||||
tp := tableprinter.New(a.io)
|
||||
c := codespace{selectedCodespace}
|
||||
formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget)
|
||||
|
||||
// Create an array of fields to display in the table with their values
|
||||
fields := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"Name", formattedName},
|
||||
{"State", c.State},
|
||||
{"Repository", c.Repository.FullName},
|
||||
{"Git Status", formatGitStatus(c)},
|
||||
{"Devcontainer Path", c.DevContainerPath},
|
||||
{"Machine Display Name", c.Machine.DisplayName},
|
||||
{"Idle Timeout", fmt.Sprintf("%d minutes", c.IdleTimeoutMinutes)},
|
||||
{"Created At", c.CreatedAt},
|
||||
{"Retention Period", formatRetentionPeriodDays(c)},
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
// Don't display the field if it is empty and we are printing to a TTY
|
||||
if !a.io.IsStdoutTTY() || field.value != "" {
|
||||
tp.AddField(field.name)
|
||||
tp.AddField(field.value)
|
||||
tp.EndRow()
|
||||
}
|
||||
}
|
||||
|
||||
err = tp.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatGitStatus(codespace codespace) string {
|
||||
branchWithGitStatus := codespace.branchWithGitStatus()
|
||||
|
||||
// Format the commits ahead/behind with proper pluralization
|
||||
commitsAhead := text.Pluralize(codespace.GitStatus.Ahead, "commit")
|
||||
commitsBehind := text.Pluralize(codespace.GitStatus.Behind, "commit")
|
||||
|
||||
return fmt.Sprintf("%s - %s ahead, %s behind", branchWithGitStatus, commitsAhead, commitsBehind)
|
||||
}
|
||||
|
||||
func formatRetentionPeriodDays(codespace codespace) string {
|
||||
days := codespace.RetentionPeriodMinutes / minutesInDay
|
||||
// Don't display the retention period if it is 0 days
|
||||
if days == 0 {
|
||||
return ""
|
||||
} else if days == 1 {
|
||||
return "1 day"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d days", days)
|
||||
}
|
||||
131
pkg/cmd/codespace/view_test.go
Normal file
131
pkg/cmd/codespace/view_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
)
|
||||
|
||||
func Test_NewCmdView(t *testing.T) {
|
||||
tests := []struct {
|
||||
tName string
|
||||
codespaceName string
|
||||
opts *viewOptions
|
||||
cliArgs []string
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
tName: "selector throws because no terminal found",
|
||||
opts: &viewOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "choosing codespace: error getting answers: no terminal",
|
||||
},
|
||||
{
|
||||
tName: "command fails because provided codespace doesn't exist",
|
||||
codespaceName: "i-dont-exist",
|
||||
opts: &viewOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "getting full codespace details: codespace not found",
|
||||
},
|
||||
{
|
||||
tName: "command succeeds because codespace exists (no details)",
|
||||
codespaceName: "monalisa-cli-cli-abcdef",
|
||||
opts: &viewOptions{},
|
||||
wantErr: false,
|
||||
wantStdout: "Name\tmonalisa-cli-cli-abcdef\nState\t\nRepository\t\nGit Status\t - 0 commits ahead, 0 commits behind\nDevcontainer Path\t\nMachine Display Name\t\nIdle Timeout\t0 minutes\nCreated At\t\nRetention Period\t\n",
|
||||
},
|
||||
{
|
||||
tName: "command succeeds because codespace exists (with details)",
|
||||
codespaceName: "monalisa-cli-cli-hijklm",
|
||||
opts: &viewOptions{},
|
||||
wantErr: false,
|
||||
wantStdout: "Name\tmonalisa-cli-cli-hijklm\nState\tAvailable\nRepository\tcli/cli\nGit Status\tmain* - 1 commit ahead, 2 commits behind\nDevcontainer Path\t.devcontainer/devcontainer.json\nMachine Display Name\tTest Display Name\nIdle Timeout\t30 minutes\nCreated At\t\nRetention Period\t1 day\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.tName, func(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
a := &App{
|
||||
apiClient: testViewApiMock(),
|
||||
io: ios,
|
||||
}
|
||||
selector := &CodespaceSelector{api: a.apiClient, codespaceName: tt.codespaceName}
|
||||
tt.opts.selector = selector
|
||||
|
||||
var err error
|
||||
if tt.cliArgs == nil {
|
||||
if tt.opts.selector == nil {
|
||||
t.Fatalf("selector must be set in opts if cliArgs are not provided")
|
||||
}
|
||||
|
||||
err = a.ViewCodespace(context.Background(), tt.opts)
|
||||
} else {
|
||||
cmd := newViewCmd(a)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SetOut(ios.ErrOut)
|
||||
cmd.SetErr(ios.ErrOut)
|
||||
cmd.SetArgs(tt.cliArgs)
|
||||
_, err = cmd.ExecuteC()
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Edit() expected error, got nil")
|
||||
} else if err.Error() != tt.errMsg {
|
||||
t.Errorf("Edit() error = %q, want %q", err, tt.errMsg)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Edit() expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if out := stdout.String(); out != tt.wantStdout {
|
||||
t.Errorf("stdout = %q, want %q", out, tt.wantStdout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testViewApiMock() *apiClientMock {
|
||||
codespaceWithNoDetails := &api.Codespace{
|
||||
Name: "monalisa-cli-cli-abcdef",
|
||||
}
|
||||
codespaceWithDetails := &api.Codespace{
|
||||
Name: "monalisa-cli-cli-hijklm",
|
||||
GitStatus: api.CodespaceGitStatus{
|
||||
Ahead: 1,
|
||||
Behind: 2,
|
||||
Ref: "main",
|
||||
HasUnpushedChanges: true,
|
||||
HasUncommittedChanges: true,
|
||||
},
|
||||
IdleTimeoutMinutes: 30,
|
||||
RetentionPeriodMinutes: 1440,
|
||||
State: "Available",
|
||||
Repository: api.Repository{FullName: "cli/cli"},
|
||||
DevContainerPath: ".devcontainer/devcontainer.json",
|
||||
Machine: api.CodespaceMachine{
|
||||
DisplayName: "Test Display Name",
|
||||
},
|
||||
}
|
||||
return &apiClientMock{
|
||||
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
|
||||
if name == codespaceWithDetails.Name {
|
||||
return codespaceWithDetails, nil
|
||||
} else if name == codespaceWithNoDetails.Name {
|
||||
return codespaceWithNoDetails, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("codespace not found")
|
||||
},
|
||||
ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
|
||||
return []*api.Codespace{codespaceWithNoDetails, codespaceWithDetails}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -84,43 +84,43 @@ func Test_getExtensionRepos(t *testing.T) {
|
|||
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(search.RepositoriesResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Repository{
|
||||
{
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
Description: "terminal animations",
|
||||
Owner: search.User{
|
||||
Login: "vilmibm",
|
||||
httpmock.JSONResponse(map[string]interface{}{
|
||||
"incomplete_results": false,
|
||||
"total_count": 4,
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "gh-screensaver",
|
||||
"full_name": "vilmibm/gh-screensaver",
|
||||
"description": "terminal animations",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "vilmibm",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
Description: "it's just cool ok",
|
||||
Owner: search.User{
|
||||
Login: "cli",
|
||||
map[string]interface{}{
|
||||
"name": "gh-cool",
|
||||
"full_name": "cli/gh-cool",
|
||||
"description": "it's just cool ok",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
Description: "helps with triage",
|
||||
Owner: search.User{
|
||||
Login: "samcoe",
|
||||
map[string]interface{}{
|
||||
"name": "gh-triage",
|
||||
"full_name": "samcoe/gh-triage",
|
||||
"description": "helps with triage",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "samcoe",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
Description: "something something enterprise",
|
||||
Owner: search.User{
|
||||
Login: "github",
|
||||
map[string]interface{}{
|
||||
"name": "gh-gei",
|
||||
"full_name": "github/gh-gei",
|
||||
"description": "something something enterprise",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 4,
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -643,10 +643,8 @@ func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager,
|
|||
}
|
||||
|
||||
commandName := strings.TrimPrefix(extName, "gh-")
|
||||
if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil {
|
||||
return nil, err
|
||||
} else if c != rootCmd {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command", commandName)
|
||||
if c, _, _ := rootCmd.Find([]string{commandName}); c != rootCmd && c.GroupID != "extension" {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command or alias", commandName)
|
||||
}
|
||||
|
||||
for _, ext := range m.List() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -74,7 +73,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
}
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(searchResults()),
|
||||
httpmock.JSONResponse(searchResults(4)),
|
||||
)
|
||||
},
|
||||
isTTY: true,
|
||||
|
|
@ -111,7 +110,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
}
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(searchResults()),
|
||||
httpmock.JSONResponse(searchResults(4)),
|
||||
)
|
||||
},
|
||||
wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n\tcli/gh-cool\tit's just cool ok\n\tsamcoe/gh-triage\thelps with triage\ninstalled\tgithub/gh-gei\tsomething something enterprise\n",
|
||||
|
|
@ -145,9 +144,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"30"},
|
||||
"q": []string{"screen topic:gh-extension"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -175,9 +172,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"1"},
|
||||
"q": []string{"topic:gh-extension"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -203,9 +198,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"30"},
|
||||
"q": []string{"license:GPLv3 topic:gh-extension user:jillvalentine"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -974,7 +967,7 @@ func Test_checkValidExtension(t *testing.T) {
|
|||
manager: m,
|
||||
extName: "gh-auth",
|
||||
},
|
||||
wantError: "\"auth\" matches the name of a built-in command",
|
||||
wantError: "\"auth\" matches the name of a built-in command or alias",
|
||||
},
|
||||
{
|
||||
name: "clashes with an installed extension",
|
||||
|
|
@ -998,43 +991,48 @@ func Test_checkValidExtension(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func searchResults() search.RepositoriesResult {
|
||||
return search.RepositoriesResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Repository{
|
||||
{
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
Description: "terminal animations",
|
||||
Owner: search.User{
|
||||
Login: "vilmibm",
|
||||
func searchResults(numResults int) interface{} {
|
||||
result := map[string]interface{}{
|
||||
"incomplete_results": false,
|
||||
"total_count": 4,
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "gh-screensaver",
|
||||
"full_name": "vilmibm/gh-screensaver",
|
||||
"description": "terminal animations",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "vilmibm",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
Description: "it's just cool ok",
|
||||
Owner: search.User{
|
||||
Login: "cli",
|
||||
map[string]interface{}{
|
||||
"name": "gh-cool",
|
||||
"full_name": "cli/gh-cool",
|
||||
"description": "it's just cool ok",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
Description: "helps with triage",
|
||||
Owner: search.User{
|
||||
Login: "samcoe",
|
||||
map[string]interface{}{
|
||||
"name": "gh-triage",
|
||||
"full_name": "samcoe/gh-triage",
|
||||
"description": "helps with triage",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "samcoe",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
Description: "something something enterprise",
|
||||
Owner: search.User{
|
||||
Login: "github",
|
||||
map[string]interface{}{
|
||||
"name": "gh-gei",
|
||||
"full_name": "github/gh-gei",
|
||||
"description": "something something enterprise",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 4,
|
||||
}
|
||||
if len(result["items"].([]interface{})) > numResults {
|
||||
fewerItems := result["items"].([]interface{})[0:numResults]
|
||||
result["items"] = fewerItems
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/go-gh"
|
||||
"github.com/cli/go-gh/v2/pkg/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("hi world, this is the %s extension!")
|
||||
client, err := gh.RESTClient(nil)
|
||||
client, err := api.DefaultRESTClient()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const manifestName = "manifest.yml"
|
||||
|
|
@ -12,16 +20,22 @@ type ExtensionKind int
|
|||
const (
|
||||
GitKind ExtensionKind = iota
|
||||
BinaryKind
|
||||
LocalKind
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
path string
|
||||
path string
|
||||
kind ExtensionKind
|
||||
gitClient gitClient
|
||||
httpClient *http.Client
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// These fields get resolved dynamically:
|
||||
url string
|
||||
isLocal bool
|
||||
isPinned bool
|
||||
isPinned *bool
|
||||
currentVersion string
|
||||
latestVersion string
|
||||
kind ExtensionKind
|
||||
}
|
||||
|
||||
func (e *Extension) Name() string {
|
||||
|
|
@ -32,32 +46,157 @@ func (e *Extension) Path() string {
|
|||
return e.path
|
||||
}
|
||||
|
||||
func (e *Extension) URL() string {
|
||||
return e.url
|
||||
}
|
||||
|
||||
func (e *Extension) IsLocal() bool {
|
||||
return e.isLocal
|
||||
}
|
||||
|
||||
func (e *Extension) CurrentVersion() string {
|
||||
return e.currentVersion
|
||||
}
|
||||
|
||||
func (e *Extension) IsPinned() bool {
|
||||
return e.isPinned
|
||||
}
|
||||
|
||||
func (e *Extension) UpdateAvailable() bool {
|
||||
if e.isLocal ||
|
||||
e.currentVersion == "" ||
|
||||
e.latestVersion == "" ||
|
||||
e.currentVersion == e.latestVersion {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return e.kind == LocalKind
|
||||
}
|
||||
|
||||
func (e *Extension) IsBinary() bool {
|
||||
return e.kind == BinaryKind
|
||||
}
|
||||
|
||||
func (e *Extension) URL() string {
|
||||
e.mu.RLock()
|
||||
if e.url != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.url
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var url string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
repo := ghrepo.NewWithHost(manifest.Owner, manifest.Name, manifest.Host)
|
||||
url = ghrepo.GenerateRepoURL(repo, "")
|
||||
}
|
||||
case GitKind:
|
||||
if remoteURL, err := e.gitClient.Config("remote.origin.url"); err == nil {
|
||||
url = strings.TrimSpace(string(remoteURL))
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.url = url
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.url
|
||||
}
|
||||
|
||||
func (e *Extension) CurrentVersion() string {
|
||||
e.mu.RLock()
|
||||
if e.currentVersion != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.currentVersion
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var currentVersion string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
currentVersion = manifest.Tag
|
||||
}
|
||||
case GitKind:
|
||||
if sha, err := e.gitClient.CommandOutput([]string{"rev-parse", "HEAD"}); err == nil {
|
||||
currentVersion = string(bytes.TrimSpace(sha))
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.currentVersion = currentVersion
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.currentVersion
|
||||
}
|
||||
|
||||
func (e *Extension) LatestVersion() string {
|
||||
e.mu.RLock()
|
||||
if e.latestVersion != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.latestVersion
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var latestVersion string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
repo, err := ghrepo.FromFullName(e.URL())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
release, err := fetchLatestRelease(e.httpClient, repo)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
latestVersion = release.Tag
|
||||
case GitKind:
|
||||
if lsRemote, err := e.gitClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"}); err == nil {
|
||||
latestVersion = string(bytes.SplitN(lsRemote, []byte("\t"), 2)[0])
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.latestVersion = latestVersion
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.latestVersion
|
||||
}
|
||||
|
||||
func (e *Extension) IsPinned() bool {
|
||||
e.mu.RLock()
|
||||
if e.isPinned != nil {
|
||||
defer e.mu.RUnlock()
|
||||
return *e.isPinned
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var isPinned bool
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
isPinned = manifest.IsPinned
|
||||
}
|
||||
case GitKind:
|
||||
pinPath := filepath.Join(e.Path(), fmt.Sprintf(".pin-%s", e.CurrentVersion()))
|
||||
if _, err := os.Stat(pinPath); err == nil {
|
||||
isPinned = true
|
||||
} else {
|
||||
isPinned = false
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.isPinned = &isPinned
|
||||
e.mu.Unlock()
|
||||
|
||||
return *e.isPinned
|
||||
}
|
||||
|
||||
func (e *Extension) UpdateAvailable() bool {
|
||||
if e.IsLocal() ||
|
||||
e.CurrentVersion() == "" ||
|
||||
e.LatestVersion() == "" ||
|
||||
e.CurrentVersion() == e.LatestVersion() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *Extension) loadManifest() (binManifest, error) {
|
||||
var bm binManifest
|
||||
dir, _ := filepath.Split(e.Path())
|
||||
manifestPath := filepath.Join(dir, manifestName)
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return bm, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
|
||||
}
|
||||
err = yaml.Unmarshal(manifest, &bm)
|
||||
if err != nil {
|
||||
return bm, fmt.Errorf("could not parse %s: %w", manifestPath, err)
|
||||
}
|
||||
return bm, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func TestUpdateAvailable_IsLocal(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: true,
|
||||
kind: LocalKind,
|
||||
}
|
||||
|
||||
assert.False(t, e.UpdateAvailable())
|
||||
|
|
@ -16,7 +16,7 @@ func TestUpdateAvailable_IsLocal(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: LocalKind,
|
||||
}
|
||||
|
||||
assert.False(t, e.UpdateAvailable())
|
||||
|
|
@ -24,7 +24,7 @@ func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "1.0.0",
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "1.1.0",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,16 +46,9 @@ func (g *gitExecuter) Fetch(remote string, refspec string) error {
|
|||
}
|
||||
|
||||
func (g *gitExecuter) ForRepo(repoDir string) gitClient {
|
||||
return &gitExecuter{
|
||||
client: &git.Client{
|
||||
GhPath: g.client.GhPath,
|
||||
RepoDir: repoDir,
|
||||
GitPath: g.client.GitPath,
|
||||
Stderr: g.client.Stderr,
|
||||
Stdin: g.client.Stdin,
|
||||
Stdout: g.client.Stdout,
|
||||
},
|
||||
}
|
||||
gc := g.client.Copy()
|
||||
gc.RepoDir = repoDir
|
||||
return &gitExecuter{client: gc}
|
||||
}
|
||||
|
||||
func (g *gitExecuter) Pull(remote, branch string) error {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -83,7 +81,7 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
forwardArgs := args[1:]
|
||||
|
||||
exts, _ := m.list(false)
|
||||
var ext Extension
|
||||
var ext *Extension
|
||||
for _, e := range exts {
|
||||
if e.Name() == extName {
|
||||
ext = e
|
||||
|
|
@ -121,39 +119,53 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
func (m *Manager) List() []extensions.Extension {
|
||||
exts, _ := m.list(false)
|
||||
r := make([]extensions.Extension, len(exts))
|
||||
for i, v := range exts {
|
||||
val := v
|
||||
r[i] = &val
|
||||
for i, ext := range exts {
|
||||
r[i] = ext
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
|
||||
func (m *Manager) list(includeMetadata bool) ([]*Extension, error) {
|
||||
dir := m.installDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []Extension
|
||||
results := make([]*Extension, 0, len(entries))
|
||||
for _, f := range entries {
|
||||
if !strings.HasPrefix(f.Name(), "gh-") {
|
||||
continue
|
||||
}
|
||||
var ext Extension
|
||||
var err error
|
||||
if f.IsDir() {
|
||||
ext, err = m.parseExtensionDir(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if _, err := os.Stat(filepath.Join(dir, f.Name(), manifestName)); err == nil {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: BinaryKind,
|
||||
httpClient: m.client,
|
||||
})
|
||||
} else {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: GitKind,
|
||||
gitClient: m.gitClient.ForRepo(filepath.Join(dir, f.Name())),
|
||||
})
|
||||
}
|
||||
results = append(results, ext)
|
||||
} else if isSymlink(f.Type()) {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: LocalKind,
|
||||
})
|
||||
} else {
|
||||
ext, err = m.parseExtensionFile(f)
|
||||
// the contents of a regular file point to a local extension on disk
|
||||
p, err := readPathFromFile(filepath.Join(dir, f.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, ext)
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(p, f.Name()),
|
||||
kind: LocalKind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,145 +176,16 @@ func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
|
|||
return results, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseExtensionFile(fi fs.DirEntry) (Extension, error) {
|
||||
ext := Extension{isLocal: true}
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
if !isSymlink(fi.Type()) {
|
||||
// if this is a regular file, its contents is the local directory of the extension
|
||||
p, err := readPathFromFile(filepath.Join(id, fi.Name()))
|
||||
if err != nil {
|
||||
return ext, err
|
||||
}
|
||||
exePath = filepath.Join(p, fi.Name())
|
||||
}
|
||||
ext.path = exePath
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil {
|
||||
return m.parseBinaryExtensionDir(fi)
|
||||
}
|
||||
|
||||
return m.parseGitExtensionDir(fi)
|
||||
}
|
||||
|
||||
func (m *Manager) parseBinaryExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
ext := Extension{path: exePath, kind: BinaryKind}
|
||||
manifestPath := filepath.Join(id, fi.Name(), manifestName)
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
|
||||
}
|
||||
var bm binManifest
|
||||
err = yaml.Unmarshal(manifest, &bm)
|
||||
if err != nil {
|
||||
return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err)
|
||||
}
|
||||
repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host)
|
||||
remoteURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
ext.url = remoteURL
|
||||
ext.currentVersion = bm.Tag
|
||||
ext.isPinned = bm.IsPinned
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
remoteUrl := m.getRemoteUrl(fi.Name())
|
||||
currentVersion := m.getCurrentVersion(fi.Name())
|
||||
|
||||
var isPinned bool
|
||||
pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion))
|
||||
if _, err := os.Stat(pinPath); err == nil {
|
||||
isPinned = true
|
||||
}
|
||||
|
||||
return Extension{
|
||||
path: exePath,
|
||||
url: remoteUrl,
|
||||
isLocal: false,
|
||||
currentVersion: currentVersion,
|
||||
kind: GitKind,
|
||||
isPinned: isPinned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getCurrentVersion determines the current version for non-local git extensions.
|
||||
func (m *Manager) getCurrentVersion(extension string) string {
|
||||
dir := filepath.Join(m.installDir(), extension)
|
||||
scopedClient := m.gitClient.ForRepo(dir)
|
||||
localSha, err := scopedClient.CommandOutput([]string{"rev-parse", "HEAD"})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(bytes.TrimSpace(localSha))
|
||||
}
|
||||
|
||||
// getRemoteUrl determines the remote URL for non-local git extensions.
|
||||
func (m *Manager) getRemoteUrl(extension string) string {
|
||||
dir := filepath.Join(m.installDir(), extension)
|
||||
scopedClient := m.gitClient.ForRepo(dir)
|
||||
url, err := scopedClient.Config("remote.origin.url")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(url))
|
||||
}
|
||||
|
||||
func (m *Manager) populateLatestVersions(exts []Extension) {
|
||||
size := len(exts)
|
||||
type result struct {
|
||||
index int
|
||||
version string
|
||||
}
|
||||
ch := make(chan result, size)
|
||||
func (m *Manager) populateLatestVersions(exts []*Extension) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(size)
|
||||
for idx, ext := range exts {
|
||||
go func(i int, e Extension) {
|
||||
for _, ext := range exts {
|
||||
wg.Add(1)
|
||||
go func(e *Extension) {
|
||||
defer wg.Done()
|
||||
version, _ := m.getLatestVersion(e)
|
||||
ch <- result{index: i, version: version}
|
||||
}(idx, ext)
|
||||
e.LatestVersion()
|
||||
}(ext)
|
||||
}
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
for r := range ch {
|
||||
ext := &exts[r.index]
|
||||
ext.latestVersion = r.version
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) getLatestVersion(ext Extension) (string, error) {
|
||||
if ext.isLocal {
|
||||
return "", localExtensionUpgradeError
|
||||
}
|
||||
if ext.IsBinary() {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Tag, nil
|
||||
} else {
|
||||
extDir := filepath.Dir(ext.path)
|
||||
scopedClient := m.gitClient.ForRepo(extDir)
|
||||
lsRemote, err := scopedClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0]
|
||||
return string(remoteSha), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) InstallLocal(dir string) error {
|
||||
|
|
@ -521,22 +404,23 @@ func (m *Manager) Upgrade(name string, force bool) error {
|
|||
if f.Name() != name {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
// For single extensions manually retrieve latest version since we forgo
|
||||
// doing it during list.
|
||||
f.latestVersion, err = m.getLatestVersion(f)
|
||||
if err != nil {
|
||||
return err
|
||||
if f.IsLocal() {
|
||||
return localExtensionUpgradeError
|
||||
}
|
||||
return m.upgradeExtensions([]Extension{f}, force)
|
||||
// For single extensions manually retrieve latest version since we forgo doing it during list.
|
||||
if latestVersion := f.LatestVersion(); latestVersion == "" {
|
||||
return fmt.Errorf("unable to retrieve latest version for extension %q", name)
|
||||
}
|
||||
return m.upgradeExtensions([]*Extension{f}, force)
|
||||
}
|
||||
return fmt.Errorf("no extension matched %q", name)
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
||||
func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error {
|
||||
var failed bool
|
||||
for _, f := range exts {
|
||||
fmt.Fprintf(m.io.Out, "[%s]: ", f.Name())
|
||||
currentVersion := displayExtensionVersion(f, f.CurrentVersion())
|
||||
err := m.upgradeExtension(f, force)
|
||||
if err != nil {
|
||||
if !errors.Is(err, localExtensionUpgradeError) &&
|
||||
|
|
@ -547,8 +431,7 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
|||
fmt.Fprintf(m.io.Out, "%s\n", err)
|
||||
continue
|
||||
}
|
||||
currentVersion := displayExtensionVersion(&f, f.currentVersion)
|
||||
latestVersion := displayExtensionVersion(&f, f.latestVersion)
|
||||
latestVersion := displayExtensionVersion(f, f.LatestVersion())
|
||||
if m.dryRunMode {
|
||||
fmt.Fprintf(m.io.Out, "would have upgraded from %s to %s\n", currentVersion, latestVersion)
|
||||
} else {
|
||||
|
|
@ -561,8 +444,8 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
||||
if ext.isLocal {
|
||||
func (m *Manager) upgradeExtension(ext *Extension, force bool) error {
|
||||
if ext.IsLocal() {
|
||||
return localExtensionUpgradeError
|
||||
}
|
||||
if !force && ext.IsPinned() {
|
||||
|
|
@ -592,7 +475,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
|
||||
func (m *Manager) upgradeGitExtension(ext *Extension, force bool) error {
|
||||
if m.dryRunMode {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -611,10 +494,10 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
|
|||
return scopedClient.Pull("", "")
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeBinExtension(ext Extension) error {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
func (m *Manager) upgradeBinExtension(ext *Extension) error {
|
||||
repo, err := ghrepo.FromFullName(ext.URL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse URL %s: %w", ext.url, err)
|
||||
return fmt.Errorf("failed to parse URL %s: %w", ext.URL(), err)
|
||||
}
|
||||
return m.installBin(repo, "")
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue