├── .dockerignore ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docker.yml │ ├── go-ci.yml │ ├── go-lint.yml │ ├── goreleaser-check.yml │ ├── goreleaser.yml │ ├── pr-auditor.yml │ └── scip.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser-patch.yml ├── .goreleaser.yml ├── .tool-versions ├── .vscode └── settings.json ├── AUTH_PROXY.md ├── BUILD.bazel ├── CHANGELOG.md ├── CODEOWNERS ├── DEVELOPMENT.md ├── Dockerfile ├── Dockerfile.release ├── LICENSE ├── README.md ├── WINDOWS.md ├── WORKSPACE ├── cmd └── src │ ├── .gitignore │ ├── BUILD.bazel │ ├── admin.go │ ├── admin_create.go │ ├── api.go │ ├── batch.go │ ├── batch_apply.go │ ├── batch_common.go │ ├── batch_exec.go │ ├── batch_new.go │ ├── batch_preview.go │ ├── batch_remote.go │ ├── batch_repositories.go │ ├── batch_validate.go │ ├── cmd.go │ ├── code_intel.go │ ├── code_intel_upload.go │ ├── code_intel_upload_flags.go │ ├── code_intel_upload_flags_test.go │ ├── code_intel_upload_transfer.go │ ├── code_intel_upload_vendored.go │ ├── codeowners.go │ ├── codeowners_create.go │ ├── codeowners_delete.go │ ├── codeowners_get.go │ ├── codeowners_update.go │ ├── colors.go │ ├── config.go │ ├── config_edit.go │ ├── config_get.go │ ├── config_list.go │ ├── debug.go │ ├── debug_common.go │ ├── debug_compose.go │ ├── debug_kube.go │ ├── debug_server.go │ ├── doc.go │ ├── extensions.go │ ├── extensions_copy.go │ ├── extensions_delete.go │ ├── extensions_get.go │ ├── extensions_list.go │ ├── extensions_publish.go │ ├── extensions_publish_test.go │ ├── extsvc.go │ ├── extsvc_create.go │ ├── extsvc_edit.go │ ├── extsvc_list.go │ ├── format.go │ ├── gateway.go │ ├── gateway_benchmark.go │ ├── gateway_benchmark_stream.go │ ├── headers.go │ ├── headers_test.go │ ├── login.go │ ├── login_test.go │ ├── main.go │ ├── main_test.go │ ├── orgs.go │ ├── orgs_create.go │ ├── orgs_delete.go │ ├── orgs_get.go │ ├── orgs_list.go │ ├── orgs_members.go │ ├── orgs_members_add.go │ ├── orgs_members_remove.go │ ├── repos.go │ ├── repos_add_metadata.go │ ├── repos_delete.go │ ├── repos_delete_metadata.go │ ├── repos_get.go │ ├── repos_list.go │ ├── repos_update_metadata.go │ ├── sbom.go │ ├── sbom_fetch.go │ ├── sbom_utils.go │ ├── scout.go │ ├── scout_advise.go │ ├── scout_resource.go │ ├── scout_usage.go │ ├── search.go │ ├── search_alert.go │ ├── search_alert_test.go │ ├── search_jobs.go │ ├── search_jobs_cancel.go │ ├── search_jobs_create.go │ ├── search_jobs_delete.go │ ├── search_jobs_get.go │ ├── search_jobs_list.go │ ├── search_jobs_logs.go │ ├── search_jobs_restart.go │ ├── search_jobs_results.go │ ├── search_stream.go │ ├── search_stream_test.go │ ├── search_test.go │ ├── servegit.go │ ├── signature.go │ ├── signature_fetch.go │ ├── snapshot.go │ ├── snapshot_databases.go │ ├── snapshot_restore.go │ ├── snapshot_summary.go │ ├── snapshot_testcmd.go │ ├── snapshot_upload.go │ ├── team_members_list.go │ ├── teams.go │ ├── teams_create.go │ ├── teams_delete.go │ ├── teams_list.go │ ├── teams_members.go │ ├── teams_members_add.go │ ├── teams_members_remove.go │ ├── teams_update.go │ ├── test.sh │ ├── testdata │ ├── TestSearchStream │ │ ├── JSON.golden │ │ └── Text.golden │ └── search_formatting │ │ ├── basic-commit-new.test.json │ │ ├── basic-commit-new.want.txt │ │ ├── basic-commit.test.json │ │ ├── basic-commit.want.txt │ │ ├── basic-diff-new.test.json │ │ ├── basic-diff-new.want.txt │ │ ├── basic-diff.test.json │ │ ├── basic-diff.want.txt │ │ ├── basic-repo-new.test.json │ │ ├── basic-repo-new.want.txt │ │ ├── basic-repo.test.json │ │ ├── basic-repo.want.txt │ │ ├── basic.test.json │ │ ├── basic.want.txt │ │ ├── cloning_missing_timedout.test.json │ │ ├── cloning_missing_timedout.want.txt │ │ ├── cloning_multiple_repo.test.json │ │ ├── cloning_multiple_repo.want.txt │ │ ├── cloning_repo.test.json │ │ ├── cloning_repo.want.txt │ │ ├── highlight-bug.test.json │ │ ├── highlight-bug.want.txt │ │ ├── highlight-structural-search.test.json │ │ └── highlight-structural-search.want.txt │ ├── users.go │ ├── users_create.go │ ├── users_delete.go │ ├── users_get.go │ ├── users_list.go │ ├── users_prune.go │ ├── users_tag.go │ ├── validate.go │ ├── validate_install.go │ ├── validate_kube.go │ └── version.go ├── contrib ├── .gitignore ├── default.nix ├── flake.lock └── flake.nix ├── deps.bzl ├── dev ├── .gitignore ├── go-lint.sh ├── golangci-lint.sh └── test-proxies.sh ├── docker └── batch-change-volume-workspace │ ├── Dockerfile │ ├── README.md │ └── push.py ├── go.mod ├── go.sum ├── internal ├── api │ ├── BUILD.bazel │ ├── api.go │ ├── api_test.go │ ├── errors.go │ ├── errors_test.go │ ├── flags.go │ ├── gzip.go │ ├── gzip_test.go │ ├── mock │ │ ├── BUILD.bazel │ │ └── api.go │ ├── nullable.go │ ├── proxy.go │ └── test_unix_socket_server.go ├── batches │ ├── BUILD.bazel │ ├── debug.go │ ├── docker │ │ ├── BUILD.bazel │ │ ├── cache.go │ │ ├── cache_test.go │ │ ├── context.go │ │ ├── image.go │ │ ├── image_test.go │ │ ├── info.go │ │ ├── info_test.go │ │ ├── main_test.go │ │ └── version.go │ ├── errors.go │ ├── executor │ │ ├── BUILD.bazel │ │ ├── coordinator.go │ │ ├── coordinator_test.go │ │ ├── execution_cache.go │ │ ├── execution_cache_test.go │ │ ├── executor.go │ │ ├── executor_test.go │ │ ├── main_test.go │ │ ├── run_steps.go │ │ ├── task.go │ │ ├── task_test.go │ │ ├── testdata │ │ │ └── dummydocker │ │ │ │ └── docker │ │ └── ui.go │ ├── features.go │ ├── graphql │ │ ├── BUILD.bazel │ │ ├── batches.go │ │ └── repository.go │ ├── license.go │ ├── log │ │ ├── BUILD.bazel │ │ ├── disk_manager.go │ │ ├── disk_task_logger.go │ │ ├── logger.go │ │ └── noop_task_logger.go │ ├── mock │ │ ├── BUILD.bazel │ │ ├── cache.go │ │ ├── docker_image_progress.go │ │ ├── image.go │ │ ├── logger.go │ │ └── repo_archive.go │ ├── repozip │ │ ├── BUILD.bazel │ │ ├── fetcher.go │ │ ├── fetcher_test.go │ │ └── noop.go │ ├── service │ │ ├── BUILD.bazel │ │ ├── build_tasks.go │ │ ├── remote.go │ │ ├── remote_norace_test.go │ │ ├── remote_test.go │ │ ├── remote_windows_test.go │ │ ├── service.go │ │ └── service_test.go │ ├── ui │ │ ├── BUILD.bazel │ │ ├── exec_ui.go │ │ ├── interval_writer.go │ │ ├── interval_writer_test.go │ │ ├── json_lines.go │ │ ├── task_exec_tui.go │ │ ├── task_exec_tui_test.go │ │ ├── tty.go │ │ └── tui.go │ ├── util │ │ ├── BUILD.bazel │ │ └── repo.go │ ├── watchdog │ │ ├── BUILD.bazel │ │ ├── watchdog.go │ │ └── watchdog_test.go │ └── workspace │ │ ├── BUILD.bazel │ │ ├── bind_workspace.go │ │ ├── bind_workspace_nonwin_test.go │ │ ├── bind_workspace_test.go │ │ ├── bind_workspace_windows_test.go │ │ ├── executor_workspace.go │ │ ├── git.go │ │ ├── main_test.go │ │ ├── volume_workspace.go │ │ ├── volume_workspace_test.go │ │ ├── workspace.go │ │ └── workspace_test.go ├── cmderrors │ ├── BUILD.bazel │ └── errors.go ├── codeintel │ ├── BUILD.bazel │ ├── gitutil.go │ ├── gitutil_test.go │ ├── sanitation.go │ └── sanitation_test.go ├── exec │ ├── BUILD.bazel │ ├── exec.go │ ├── expect │ │ ├── BUILD.bazel │ │ └── expect.go │ └── middleware.go ├── instancehealth │ ├── BUILD.bazel │ ├── checks.go │ ├── checks_test.go │ └── summary.go ├── lazyregexp │ ├── BUILD.bazel │ └── lazyregexp.go ├── pgdump │ ├── BUILD.bazel │ ├── extensions.go │ ├── extensions_test.go │ └── pgdump.go ├── scout │ ├── advise │ │ ├── advise.go │ │ ├── advise_test.go │ │ └── k8s.go │ ├── constants.go │ ├── helpers.go │ ├── kube │ │ ├── kube.go │ │ └── kube_test.go │ ├── resource │ │ ├── resource.go │ │ └── resource_test.go │ ├── style │ │ ├── resource_table.go │ │ └── usage_table.go │ ├── types.go │ └── usage │ │ ├── k8s.go │ │ ├── usage.go │ │ └── usage_test.go ├── servegit │ ├── BUILD.bazel │ ├── serve.go │ └── serve_test.go ├── streaming │ ├── BUILD.bazel │ ├── api.go │ ├── client.go │ ├── client_test.go │ ├── events.go │ ├── search.go │ └── writer.go ├── users │ ├── BUILD.bazel │ └── admin.go ├── validate │ ├── BUILD.bazel │ ├── README.md │ ├── install │ │ ├── BUILD.bazel │ │ ├── config.go │ │ ├── github.go │ │ ├── insight.go │ │ └── install.go │ ├── kube │ │ ├── BUILD.bazel │ │ ├── aks.go │ │ ├── eks.go │ │ ├── eks_test.go │ │ ├── gke.go │ │ ├── kube.go │ │ └── kube_test.go │ └── validate.go └── version │ ├── BUILD.bazel │ └── version.go ├── npm-distribution ├── .gitignore ├── copy-files.js ├── install.js ├── package.json ├── src.js └── yarn.lock ├── release.sh └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Anything Git or VSCode related is definitely irrelevant for a build. 2 | .git* 3 | .vscode 4 | 5 | # The docker directory contains other Docker images. 6 | docker 7 | 8 | # The release directory is created by goreleaser and isn't required here. 9 | release 10 | 11 | # Documentation and examples aren't needed. 12 | *.md 13 | *.{yaml,yml} 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Test plan 2 | 3 | 10 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # For more information, refer to the "Dependent Docker images" section of 2 | # DEVELOPMENT.md. 3 | name: Publish Docker image dependencies 4 | 5 | # We only want to build on releases; this condition is 100% stolen from the 6 | # goreleaser action. 7 | on: 8 | workflow_dispatch: 9 | push: 10 | tags: 11 | - "*" 12 | - "!latest" 13 | 14 | jobs: 15 | publish: 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | # We need buildx to be able to build a multi-architecture image. 22 | - name: Set up Docker buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | # We also need QEMU, since this is running on an AMD64 host and we want to 26 | # build ARM64 images. 27 | - name: Set up QEMU 28 | uses: docker/setup-qemu-action@v3 29 | with: 30 | platforms: arm64 31 | 32 | - run: ./docker/batch-change-volume-workspace/push.py -d ./docker/batch-change-volume-workspace/Dockerfile -i sourcegraph/src-batch-change-volume-workspace -p linux/amd64,linux/arm64,linux/386 --readme ./docker/batch-change-volume-workspace/README.md 33 | env: 34 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 35 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 36 | -------------------------------------------------------------------------------- /.github/workflows/go-ci.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | env: 7 | GOPRIVATE: "github.com/sourcegraph/*" 8 | PRIVATE_TOKEN: "${{ secrets.PRIVATE_SG_ACCESS_TOKEN }}" 9 | 10 | jobs: 11 | go-test: 12 | strategy: 13 | matrix: 14 | go-version: [1.24.1] 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | - name: (Windows) Enable pulling Go modules from private sourcegraph/sourcegraph 25 | if: runner.os == 'Windows' 26 | run: git config --global url."https://$env:PRIVATE_TOKEN@github.com/sourcegraph/".insteadOf "https://github.com/sourcegraph/" 27 | - name: (Default) Enable pulling Go modules from private sourcegraph/sourcegraph 28 | if: runner.os != 'Windows' 29 | run: git config --global url."https://${PRIVATE_TOKEN}@github.com/sourcegraph/".insteadOf "https://github.com/sourcegraph/" 30 | - run: | 31 | go test -race -v ./... 32 | go test -v ./... 33 | -------------------------------------------------------------------------------- /.github/workflows/go-lint.yml: -------------------------------------------------------------------------------- 1 | name: Go Lint 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | env: 7 | GOPRIVATE: "github.com/sourcegraph/*" 8 | PRIVATE_TOKEN: "${{ secrets.PRIVATE_SG_ACCESS_TOKEN }}" 9 | 10 | jobs: 11 | go-lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.24.1 20 | - name: Enable pulling Go modules from private sourcegraph/sourcegraph 21 | run: git config --global url."https://${PRIVATE_TOKEN}@github.com/sourcegraph/".insteadOf "https://github.com/sourcegraph/" 22 | - run: ./dev/go-lint.sh 23 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser-check.yml: -------------------------------------------------------------------------------- 1 | name: GoReleaser check 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | env: 7 | GOPRIVATE: "github.com/sourcegraph/*" 8 | PRIVATE_TOKEN: "${{ secrets.PRIVATE_SG_ACCESS_TOKEN }}" 9 | 10 | jobs: 11 | goreleaser: 12 | name: check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.24.1 23 | - name: Enable pulling Go modules from private sourcegraph/sourcegraph 24 | run: git config --global url."https://${PRIVATE_TOKEN}@github.com/sourcegraph/".insteadOf "https://github.com/sourcegraph/" 25 | - name: Check GoReleaser config 26 | uses: goreleaser/goreleaser-action@v5 27 | with: 28 | version: latest 29 | args: check 30 | -------------------------------------------------------------------------------- /.github/workflows/pr-auditor.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.sourcegraph.com/dev/background-information/ci#pr-auditor 2 | name: pr-auditor 3 | on: 4 | pull_request_target: 5 | types: [ closed, edited, opened, synchronize, ready_for_review ] 6 | 7 | 8 | jobs: 9 | check-pr: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | repository: 'sourcegraph/devx-service' 15 | token: ${{ secrets.PR_AUDITOR_TOKEN }} 16 | - uses: actions/setup-go@v5 17 | with: { go-version: '1.23' } 18 | 19 | - run: 'go run ./cmd/pr-auditor' 20 | env: 21 | GITHUB_EVENT_PATH: ${{ env.GITHUB_EVENT_PATH }} 22 | GITHUB_TOKEN: ${{ secrets.PR_AUDITOR_TOKEN }} 23 | GITHUB_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 24 | -------------------------------------------------------------------------------- /.github/workflows/scip.yml: -------------------------------------------------------------------------------- 1 | name: SCIP 2 | on: 3 | workflow_dispatch: 4 | push: 5 | env: 6 | GOPRIVATE: "github.com/sourcegraph/*" 7 | PRIVATE_TOKEN: "${{ secrets.PRIVATE_SG_ACCESS_TOKEN }}" 8 | jobs: 9 | scip-go: 10 | runs-on: ubuntu-latest 11 | container: sourcegraph/scip-go 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 1.24.1 18 | 19 | - name: Set directory to safe for git 20 | run: git config --global --add safe.directory $GITHUB_WORKSPACE 21 | 22 | - name: Build src-cli 23 | run: go build -o ./src-cli ./cmd/src 24 | 25 | - name: Enable pulling Go modules from private sourcegraph/sourcegraph 26 | run: git config --global url."https://${PRIVATE_TOKEN}@github.com/sourcegraph/".insteadOf "https://github.com/sourcegraph/" 27 | 28 | - name: Generate SCIP data 29 | run: scip-go 30 | 31 | - name: Upload SCIP to Cloud 32 | run: ./src-cli code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 33 | env: 34 | SRC_ENDPOINT: https://sourcegraph.com/ 35 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_DOTCOM }} 36 | 37 | - name: Upload SCIP to S2 38 | run: ./src-cli code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 39 | env: 40 | SRC_ENDPOINT: https://sourcegraph.sourcegraph.com/ 41 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_S2 }} 42 | 43 | - name: Compress SCIP file 44 | run: gzip index.scip 45 | 46 | - name: Upload compressed SCIP to Cloud 47 | run: ./src-cli code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 48 | env: 49 | SRC_ENDPOINT: https://sourcegraph.com/ 50 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_DOTCOM }} 51 | 52 | - name: Upload compressed SCIP to S2 53 | run: ./src-cli code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 54 | env: 55 | SRC_ENDPOINT: https://sourcegraph.sourcegraph.com/ 56 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_S2 }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./src 2 | cmd/src/src 3 | *.zip 4 | release 5 | ./vendor 6 | .idea 7 | .env 8 | .envrc 9 | src-snapshot/ 10 | bazel-bin 11 | bazel-out 12 | bazel-testlogs 13 | bazel-zoekt 14 | bazel-src-cli 15 | .DS_Store 16 | samples 17 | sourcegraph-sboms/ 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # See explanation of linters at https://golangci-lint.run/usage/linters/ 2 | linters: 3 | disable-all: true 4 | enable: 5 | - bodyclose 6 | - depguard 7 | - gocritic 8 | - goimports 9 | - gosimple 10 | - govet 11 | - ineffassign 12 | - nolintlint 13 | - staticcheck 14 | - typecheck 15 | - unconvert 16 | - unused 17 | 18 | linters-settings: 19 | depguard: 20 | rules: 21 | main: 22 | deny: 23 | - pkg: "errors" 24 | desc: "Use github.com/sourcegraph/sourcegraph/lib/errors instead" 25 | - pkg: "github.com/pkg/errors" 26 | desc: "Use github.com/sourcegraph/sourcegraph/lib/errors instead" 27 | - pkg: "github.com/cockroachdb/errors" 28 | desc: "Use github.com/sourcegraph/sourcegraph/lib/errors instead" 29 | - pkg: "github.com/hashicorp/go-multierror" 30 | desc: "Use github.com/sourcegraph/sourcegraph/lib/errors instead" 31 | - pkg: "io/ioutil" 32 | desc: "The ioutil package has been deprecated" 33 | gocritic: 34 | disabled-checks: 35 | - appendAssign # Too many false positives 36 | - assignOp # Maybe worth adding, but likely not worth the noise 37 | - commentFormatting # No strong benefit 38 | - deprecatedComment # Unnecessary 39 | - exitAfterDefer # Only occurs in auxiliary tools 40 | - ifElseChain # Noisy for not much gain 41 | - singleCaseSwitch # Noisy for not much gain 42 | govet: 43 | disable: 44 | - composites 45 | forbidigo: 46 | forbid: 47 | # Use errors.New instead 48 | - 'fmt\.Errorf' 49 | 50 | issues: 51 | exclude-rules: 52 | # Exclude bodyclose lint from tests because leaking connections in tests 53 | # is a non-issue, and checking that adds unnecessary noise 54 | - path: _test\.go 55 | linters: 56 | - bodyclose 57 | 58 | run: 59 | timeout: 5m 60 | -------------------------------------------------------------------------------- /.goreleaser-patch.yml: -------------------------------------------------------------------------------- 1 | # This is the patch release configuration for goreleaser, which builds and publishes a new 2 | # patch release for an older version of src-cli. It publishes a new versioned formula in 3 | # the Homebrew tap and does not publish a "latest" image to Docker Hub. It should match 4 | # .goreleaser.yml in every way except for the brews.name template and the 5 | # dockers.image_templates list. 6 | dist: release 7 | env: 8 | - GO111MODULE=on 9 | - CGO_ENABLED=0 10 | before: 11 | hooks: 12 | - go mod download 13 | - go mod tidy 14 | - go generate ./... 15 | builds: 16 | - 17 | main: ./cmd/src/ 18 | binary: src 19 | ldflags: 20 | - -X github.com/sourcegraph/src-cli/internal/version.BuildTag={{.Version}} 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | goarch: 26 | - amd64 27 | - arm64 28 | archives: 29 | - id: tarball 30 | format: tar.gz 31 | - id: bin 32 | format: binary 33 | wrap_in_directory: false 34 | name_template: "src_{{ .Os }}_{{ .Arch }}" 35 | brews: 36 | - 37 | name: src-cli@{{ .Major }}.{{ .Minor }}.{{ .Patch }} 38 | homepage: "https://sourcegraph.com/" 39 | description: "Sourcegraph CLI" 40 | tap: 41 | owner: sourcegraph 42 | name: homebrew-src-cli 43 | # Folder inside the repository to put the formula. 44 | # Default is the root folder. 45 | folder: Formula 46 | # We need to set this so that goreleaser doesn't think the binary is called 47 | # `src-cli` 48 | install: | 49 | bin.install "src" 50 | ids: 51 | - tarball 52 | dockers: 53 | - dockerfile: Dockerfile.release 54 | image_templates: 55 | - "sourcegraph/src-cli:{{ .Tag }}" 56 | - "sourcegraph/src-cli:{{ .Major }}" 57 | - "sourcegraph/src-cli:{{ .Major }}.{{ .Minor }}" 58 | changelog: 59 | sort: asc 60 | filters: 61 | exclude: 62 | - '^docs:' 63 | - '^test:' 64 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is the main configuration for goreleaser, which builds and publishes a new latest 2 | # release. It updates the main formula in the Homebrew tap. It should match 3 | # .goreleaser-patch.yml in every way except for the brews.name template and the 4 | # dockers.image_templates list. 5 | dist: release 6 | env: 7 | - GO111MODULE=on 8 | - CGO_ENABLED=0 9 | before: 10 | hooks: 11 | - go mod download 12 | - go mod tidy 13 | - go generate ./... 14 | builds: 15 | - 16 | main: ./cmd/src/ 17 | binary: src 18 | ldflags: 19 | - -X github.com/sourcegraph/src-cli/internal/version.BuildTag={{.Version}} 20 | goos: 21 | - linux 22 | - windows 23 | - darwin 24 | goarch: 25 | - amd64 26 | - arm64 27 | archives: 28 | - id: tarball 29 | format: tar.gz 30 | - id: bin 31 | format: binary 32 | wrap_in_directory: false 33 | name_template: "src_{{ .Os }}_{{ .Arch }}" 34 | brews: 35 | - 36 | name: src-cli 37 | homepage: "https://sourcegraph.com/" 38 | description: "Sourcegraph CLI" 39 | repository: 40 | owner: sourcegraph 41 | name: homebrew-src-cli 42 | # Folder inside the repository to put the formula. 43 | # Default is the root folder. 44 | directory: Formula 45 | # We need to set this so that goreleaser doesn't think the binary is called 46 | # `src-cli` 47 | install: | 48 | bin.install "src" 49 | ids: 50 | - tarball 51 | dockers: 52 | - dockerfile: Dockerfile.release 53 | image_templates: 54 | - "sourcegraph/src-cli:{{ .Tag }}" 55 | - "sourcegraph/src-cli:{{ .Major }}" 56 | - "sourcegraph/src-cli:{{ .Major }}.{{ .Minor }}" 57 | - "sourcegraph/src-cli:latest" 58 | changelog: 59 | sort: asc 60 | filters: 61 | exclude: 62 | - '^docs:' 63 | - '^test:' 64 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.24.1 2 | shfmt 3.8.0 3 | shellcheck 0.10.0 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint", 3 | "shellformat.flag": "-i 2 -ci", 4 | "editor.formatOnSave": false, 5 | "[go]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "go.useLanguageServer": true, 9 | "gopls": { 10 | "local": "github.com/sourcegraph/src-cli" 11 | }, 12 | "cody.codebase": "github.com/sourcegraph/src-cli", 13 | } 14 | -------------------------------------------------------------------------------- /AUTH_PROXY.md: -------------------------------------------------------------------------------- 1 | # Authenticating requests behind a proxy 2 | 3 | If your instance is behind an authenticating proxy that requires additional headers, they can be supplied via environment variables as follows: 4 | 5 | ```sh 6 | SRC_HEADER_AUTHORIZATION="Bearer $(curl http://service.internal.corp)" SRC_HEADER_EXTRA=metadata src search 'foobar' 7 | ``` 8 | 9 | In this example, the headers `authorization: Bearer my-generated-token` and `extra: metadata` will be threaded to all HTTP requests to your instance. Multiple such headers can be supplied. 10 | 11 | An alternative to the above when passing in multiple headers or headers with dashes is to make use of the `SRC_HEADERS` environment variable as follow: 12 | 13 | ```sh 14 | SRC_HEADERS="AUTHORIZATION:Bearer somerandom_string\nClient-ID:client-one\nextra:metadata" 15 | ``` 16 | 17 | Note: The different header keys and values need to separated by a new line ("\n"). In the above example, the headers `authorization: Bearer somerandom_string`, `client-id: client-one` and `extra: metadata` will be threaded to all HTTP requests to your instance. 18 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | # gazelle:prefix github.com/sourcegraph/src-cli 2 | # gazelle:build_file_name BUILD.bazel 3 | load("@bazel_gazelle//:def.bzl", "gazelle") 4 | 5 | gazelle(name = "gazelle") 6 | 7 | gazelle( 8 | name = "gazelle-update-repos", 9 | args = [ 10 | "-from_file=go.mod", 11 | "-to_macro=deps.bzl%go_dependencies", 12 | "-prune", 13 | "-build_file_proto_mode=disable_global", 14 | ], 15 | command = "update-repos", 16 | ) 17 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | cmd/src/lsif* @sourcegraph/team-graph 2 | cmd/src/code_intel* @sourcegraph/team-graph 3 | 4 | cmd/src/search* @sourcegraph/search-platform 5 | 6 | cmd/src/extsvc* @sourcegraph/source 7 | cmd/src/servegit* @sourcegraph/source 8 | cmd/src/users* @sourcegraph/source 9 | 10 | cmd/src/batch* @sourcegraph/code-search 11 | 12 | # Shepherd team for everything else 13 | * @sourcegraph/code-search 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Since we're going to provide images based on Alpine, we also want to build on 2 | # Alpine, rather than relying on the ./src in the surrounding environment to be 3 | # sane. 4 | # 5 | # Nothing fancy here: we copy in the source code and build on the Alpine Go 6 | # image. Refer to .dockerignore to get a sense of what we're not going to copy. 7 | FROM golang:1.24.1-alpine@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 as builder 8 | 9 | COPY . /src 10 | WORKDIR /src 11 | RUN go build ./cmd/src 12 | 13 | # This stage should be kept in sync with Dockerfile.release. 14 | FROM sourcegraph/alpine:3.12@sha256:ce099fbcd3cf70b338fc4cb2a4e1fa9ae847de21afdb0a849a393b87d94fb174 15 | 16 | # needed for `src code-intel upload` and `src actions exec` 17 | RUN apk add --no-cache git 18 | 19 | COPY --from=builder /src/src /usr/bin/ 20 | ENTRYPOINT ["/usr/bin/src"] 21 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | # This Dockerfile is used by goreleaser when publishing a release, and is not 2 | # suitable for testing, since it depends on a src binary being at the project 3 | # root _and_ that src binary being runnable on Alpine. To test this, refer to 4 | # the main Dockerfile, which should have an identical second stage. 5 | FROM sourcegraph/alpine:3.12@sha256:ce099fbcd3cf70b338fc4cb2a4e1fa9ae847de21afdb0a849a393b87d94fb174 6 | 7 | # needed for `src code-intel upload` and `src actions exec` 8 | RUN apk add --no-cache git 9 | 10 | COPY src /usr/bin/ 11 | ENTRYPOINT ["/usr/bin/src"] 12 | -------------------------------------------------------------------------------- /WINDOWS.md: -------------------------------------------------------------------------------- 1 | # Sourcegraph CLI for Windows 2 | 3 | **Note:** Windows support is still rough around the edges. If you encounter issues, please let us know by filing an issue. 4 | 5 | ## Installation 6 | 7 | ### Latest version 8 | 9 | ### Install via PowerShell 10 | 11 | Run in PowerShell as administrator: 12 | 13 | ```powershell 14 | New-Item -ItemType Directory 'C:\Program Files\Sourcegraph' 15 | 16 | Invoke-WebRequest https://sourcegraph.com/.api/src-cli/src_windows_amd64.exe -OutFile 'C:\Program Files\Sourcegraph\src.exe' 17 | 18 | [Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', [EnvironmentVariableTarget]::Machine) + ';C:\Program Files\Sourcegraph', [EnvironmentVariableTarget]::Machine) 19 | $env:Path += ';C:\Program Files\Sourcegraph' 20 | ``` 21 | 22 | #### Install manually 23 | 24 | 1. Download the latest [src_windows_amd64.exe](https://sourcegraph.com/.api/src-cli/src_windows_amd64.exe) 25 | 2. Place the file under e.g. `C:\Program Files\Sourcegraph\src.exe` 26 | 3. Add that directory to your system PATH to access it from any command prompt 27 | 28 | ### Version compatible with your Sourcegraph instance 29 | 30 | ### Install via PowerShell 31 | 32 | Run in PowerShell as administrator, but replace `sourcegraph.example.com` with your Sourcegraph instance URL: 33 | 34 | ```powershell 35 | New-Item -ItemType Directory 'C:\Program Files\Sourcegraph' 36 | 37 | Invoke-WebRequest https://sourcegraph.example.com/.api/src-cli/src_windows_amd64.exe -OutFile 'C:\Program Files\Sourcegraph\src.exe' 38 | 39 | [Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', [EnvironmentVariableTarget]::Machine) + ';C:\Program Files\Sourcegraph', [EnvironmentVariableTarget]::Machine) 40 | $env:Path += ';C:\Program Files\Sourcegraph' 41 | ``` 42 | 43 | #### Install manually 44 | 45 | 1. Download the latest src_windows_amd64.exe from your Sourcegraph instance at e.g. https://sourcegraph.example.com/.api/src-cli/src_windows_amd64.exe (replace sourcegraph.example.com with your Sourcegraph instance URL) 46 | 2. Place the file under e.g. `C:\Program Files\Sourcegraph\src.exe` 47 | 3. Add that directory to your system PATH to access it from any command prompt 48 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | http_archive( 4 | name = "bazel_skylib", 5 | sha256 = "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94", 6 | urls = [ 7 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", 8 | "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", 9 | ], 10 | ) 11 | 12 | load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") 13 | 14 | bazel_skylib_workspace() 15 | 16 | http_archive( 17 | name = "aspect_bazel_lib", 18 | sha256 = "357dad9d212327c35d9244190ef010aad315e73ffa1bed1a29e20c372f9ca346", 19 | strip_prefix = "bazel-lib-2.7.0", 20 | url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.0/bazel-lib-v2.7.0.tar.gz", 21 | ) 22 | 23 | http_archive( 24 | name = "io_bazel_rules_go", 25 | sha256 = "f74c98d6df55217a36859c74b460e774abc0410a47cc100d822be34d5f990f16", 26 | urls = [ 27 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.47.1/rules_go-v0.47.1.zip", 28 | "https://github.com/bazelbuild/rules_go/releases/download/v0.47.1/rules_go-v0.47.1.zip", 29 | ], 30 | ) 31 | 32 | http_archive( 33 | name = "bazel_gazelle", 34 | sha256 = "75df288c4b31c81eb50f51e2e14f4763cb7548daae126817247064637fd9ea62", 35 | urls = [ 36 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", 37 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.36.0/bazel-gazelle-v0.36.0.tar.gz", 38 | ], 39 | ) 40 | 41 | # Go toolchain setup 42 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 43 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies","go_repository") 44 | load("//:deps.bzl", "go_dependencies") 45 | 46 | # gazelle:repository_macro deps.bzl%go_dependencies 47 | go_dependencies() 48 | 49 | go_rules_dependencies() 50 | 51 | go_register_toolchains( 52 | version = "1.19.6", 53 | ) 54 | 55 | gazelle_dependencies() 56 | 57 | go_repository( 58 | name = "com_github_aws_aws_sdk_go_v2_service_eks", 59 | importpath = "github.com/aws/aws-sdk-go-v2/service/eks", 60 | version = "v1.27.3", 61 | sum = "h1:jlh0AJVhauqSGaiwRJx4j0LNBGEAaGn46sA1XJyTj0E=", 62 | ) -------------------------------------------------------------------------------- /cmd/src/.gitignore: -------------------------------------------------------------------------------- 1 | *.got.txt 2 | -------------------------------------------------------------------------------- /cmd/src/admin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var adminCommands commander 9 | 10 | func init() { 11 | usage := `'src admin' is a tool that manages an initial admin user on a new Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src admin create [command options] 16 | 17 | The commands are: 18 | 19 | create create an initial admin user 20 | 21 | Use "src admin [command] -h" for more information about a command. 22 | ` 23 | 24 | flagSet := flag.NewFlagSet("admin", flag.ExitOnError) 25 | handler := func(args []string) error { 26 | adminCommands.run(flagSet, "srv admin", usage, args) 27 | return nil 28 | } 29 | 30 | commands = append(commands, &command{ 31 | flagSet: flagSet, 32 | aliases: []string{"admin"}, 33 | handler: handler, 34 | usageFunc: func() { 35 | fmt.Println(usage) 36 | }, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/src/batch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var batchCommands commander 9 | 10 | func init() { 11 | usage := `'src batch' manages batch changes on a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src batch command [command options] 16 | 17 | The commands are: 18 | 19 | apply applies a batch spec to create or update a batch 20 | change 21 | new creates a new batch spec YAML file 22 | preview creates a batch spec to be previewed or applied 23 | remote creates server side batch changes 24 | repos,repositories queries the exact repositories that a batch spec will 25 | apply to 26 | validate validates a batch spec 27 | 28 | Use "src batch [command] -h" for more information about a command. 29 | 30 | ` 31 | 32 | flagSet := flag.NewFlagSet("batch", flag.ExitOnError) 33 | handler := func(args []string) error { 34 | batchCommands.run(flagSet, "src batch", usage, args) 35 | return nil 36 | } 37 | 38 | // Register the command. 39 | commands = append(commands, &command{ 40 | flagSet: flagSet, 41 | aliases: []string{ 42 | "batchchange", 43 | "batch-change", 44 | "batchchanges", 45 | "batch-changes", 46 | "batches", 47 | }, 48 | handler: handler, 49 | usageFunc: func() { fmt.Println(usage) }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/src/batch_apply.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/cmderrors" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | 'src batch apply' is used to apply a batch spec on a Sourcegraph instance, 14 | creating or updating the described batch change if necessary. 15 | 16 | Usage: 17 | 18 | src batch apply [command options] [-f FILE] 19 | src batch apply [command options] FILE 20 | 21 | Examples: 22 | 23 | $ src batch apply -f batch.spec.yaml 24 | 25 | $ src batch apply -f batch.spec.yaml -namespace myorg 26 | 27 | $ src batch apply batch.spec.yaml 28 | 29 | ` 30 | 31 | flagSet := flag.NewFlagSet("apply", flag.ExitOnError) 32 | flags := newBatchExecuteFlags(flagSet, batchDefaultCacheDir(), batchDefaultTempDirPrefix()) 33 | 34 | handler := func(args []string) error { 35 | if err := flagSet.Parse(args); err != nil { 36 | return err 37 | } 38 | 39 | file, err := getBatchSpecFile(flagSet, &flags.file) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | ctx, cancel := contextCancelOnInterrupt(context.Background()) 45 | defer cancel() 46 | 47 | if err = executeBatchSpec(ctx, executeBatchSpecOpts{ 48 | flags: flags, 49 | client: cfg.apiClient(flags.api, flagSet.Output()), 50 | file: file, 51 | 52 | applyBatchSpec: true, 53 | }); err != nil { 54 | return cmderrors.ExitCode(1, nil) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | batchCommands = append(batchCommands, &command{ 61 | flagSet: flagSet, 62 | handler: handler, 63 | usageFunc: func() { 64 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src batch %s':\n", flagSet.Name()) 65 | flagSet.PrintDefaults() 66 | fmt.Println(usage) 67 | }, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/src/batch_new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | cliLog "log" 8 | 9 | "github.com/sourcegraph/src-cli/internal/api" 10 | "github.com/sourcegraph/src-cli/internal/batches/service" 11 | "github.com/sourcegraph/src-cli/internal/cmderrors" 12 | ) 13 | 14 | func init() { 15 | usage := ` 16 | 'src batch new' creates a new batch spec YAML, prefilled with all required 17 | fields. 18 | 19 | Usage: 20 | 21 | src batch new [-f FILE] 22 | 23 | Examples: 24 | 25 | 26 | $ src batch new -f batch.spec.yaml 27 | 28 | ` 29 | 30 | flagSet := flag.NewFlagSet("new", flag.ExitOnError) 31 | apiFlags := api.NewFlags(flagSet) 32 | 33 | var ( 34 | fileFlag = flagSet.String("f", "batch.yaml", "The name of the batch spec file to create.") 35 | skipErrors bool 36 | ) 37 | flagSet.BoolVar( 38 | &skipErrors, "skip-errors", false, 39 | "If true, errors encountered won't stop the program, but only log them.", 40 | ) 41 | 42 | handler := func(args []string) error { 43 | ctx := context.Background() 44 | 45 | if err := flagSet.Parse(args); err != nil { 46 | return err 47 | } 48 | 49 | if len(flagSet.Args()) != 0 { 50 | return cmderrors.Usage("additional arguments not allowed") 51 | } 52 | 53 | svc := service.New(&service.Opts{ 54 | Client: cfg.apiClient(apiFlags, flagSet.Output()), 55 | }) 56 | 57 | _, ffs, err := svc.DetermineLicenseAndFeatureFlags(ctx, skipErrors) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if err := validateSourcegraphVersionConstraint(ffs); err != nil { 63 | if !skipErrors { 64 | return err 65 | } else { 66 | cliLog.Printf("WARNING: %s", err) 67 | } 68 | } 69 | 70 | if err := svc.GenerateExampleSpec(ctx, *fileFlag); err != nil { 71 | return err 72 | } 73 | 74 | fmt.Printf("%s created.\n", *fileFlag) 75 | return nil 76 | } 77 | 78 | batchCommands = append(batchCommands, &command{ 79 | flagSet: flagSet, 80 | aliases: []string{}, 81 | handler: handler, 82 | usageFunc: func() { 83 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src batch %s':\n", flagSet.Name()) 84 | flagSet.PrintDefaults() 85 | fmt.Println(usage) 86 | }, 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /cmd/src/batch_preview.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/cmderrors" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | 'src batch preview' executes the steps in a batch spec and uploads it to a 14 | Sourcegraph instance, ready to be previewed and applied. 15 | 16 | Usage: 17 | 18 | src batch preview [command options] [-f FILE] 19 | src batch preview [command options] FILE 20 | 21 | Examples: 22 | 23 | $ src batch preview -f batch.spec.yaml 24 | 25 | $ src batch preview batch.spec.yaml 26 | 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("preview", flag.ExitOnError) 30 | flags := newBatchExecuteFlags(flagSet, batchDefaultCacheDir(), batchDefaultTempDirPrefix()) 31 | 32 | handler := func(args []string) error { 33 | if err := flagSet.Parse(args); err != nil { 34 | return err 35 | } 36 | 37 | file, err := getBatchSpecFile(flagSet, &flags.file) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | ctx, cancel := contextCancelOnInterrupt(context.Background()) 43 | defer cancel() 44 | 45 | if err = executeBatchSpec(ctx, executeBatchSpecOpts{ 46 | flags: flags, 47 | client: cfg.apiClient(flags.api, flagSet.Output()), 48 | file: file, 49 | 50 | // Do not apply the uploaded batch spec 51 | applyBatchSpec: false, 52 | }); err != nil { 53 | return cmderrors.ExitCode(1, nil) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | batchCommands = append(batchCommands, &command{ 60 | flagSet: flagSet, 61 | handler: handler, 62 | usageFunc: func() { 63 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src batch %s':\n", flagSet.Name()) 64 | flagSet.PrintDefaults() 65 | fmt.Println(usage) 66 | }, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/src/code_intel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var codeintelCommands commander 9 | 10 | func init() { 11 | usage := `'src code-intel' manages code intelligence data on a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src code-intel command [command options] 16 | 17 | The commands are: 18 | 19 | upload uploads a SCIP index 20 | 21 | Use "src code-intel [command] -h" for more information about a command. 22 | ` 23 | flagSet := flag.NewFlagSet("code-intel", flag.ExitOnError) 24 | handler := func(args []string) error { 25 | codeintelCommands.run(flagSet, "src code-intel", usage, args) 26 | return nil 27 | } 28 | 29 | // Register the command. 30 | commands = append(commands, &command{ 31 | flagSet: flagSet, 32 | aliases: []string{"code-intel"}, 33 | handler: handler, 34 | usageFunc: func() { 35 | fmt.Println(usage) 36 | }, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/src/code_intel_upload_flags_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "google.golang.org/protobuf/proto" 10 | 11 | "github.com/sourcegraph/scip/bindings/go/scip" 12 | ) 13 | 14 | var exampleSCIPIndex = scip.Index{ 15 | Metadata: &scip.Metadata{ 16 | TextDocumentEncoding: scip.TextEncoding_UTF8, 17 | ToolInfo: &scip.ToolInfo{ 18 | Name: "hello", 19 | Version: "1.0.0", 20 | }, 21 | }, 22 | } 23 | 24 | func exampleSCIPBytes(t *testing.T) []byte { 25 | bytes, err := proto.Marshal(&exampleSCIPIndex) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | return bytes 30 | } 31 | 32 | func createTempSCIPFile(t *testing.T, scipFileName string) string { 33 | t.Helper() 34 | dir := t.TempDir() 35 | require.NotEqual(t, "", scipFileName) 36 | scipFilePath := filepath.Join(dir, scipFileName) 37 | err := os.WriteFile(scipFilePath, exampleSCIPBytes(t), 0755) 38 | require.NoError(t, err) 39 | return scipFilePath 40 | } 41 | 42 | func TestInferIndexerNameAndVersion(t *testing.T) { 43 | name, version, err := readIndexerNameAndVersion(createTempSCIPFile(t, "index.scip")) 44 | require.NoError(t, err) 45 | require.Equal(t, "hello", name) 46 | require.Equal(t, "1.0.0", version) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/src/code_intel_upload_transfer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/sourcegraph/sourcegraph/lib/codeintel/upload" 8 | "github.com/sourcegraph/sourcegraph/lib/output" 9 | ) 10 | 11 | func UploadUncompressedIndex(ctx context.Context, filename string, httpClient upload.Client, opts upload.UploadOptions) (int, error) { 12 | originalReader, originalSize, err := openFileAndGetSize(filename) 13 | if err != nil { 14 | return 0, err 15 | } 16 | defer func() { 17 | _ = originalReader.Close() 18 | }() 19 | 20 | bars := []output.ProgressBar{{Label: "Compressing", Max: 1.0}} 21 | progress, _, cleanup := logProgress( 22 | opts.Output, 23 | bars, 24 | "Index compressed", 25 | "Failed to compress index", 26 | ) 27 | 28 | compressedFile, err := compressReaderToDisk(originalReader, originalSize, progress) 29 | if err != nil { 30 | cleanup(err) 31 | return 0, err 32 | } 33 | defer func() { 34 | _ = os.Remove(compressedFile) 35 | }() 36 | 37 | compressedSize, err := getFileSize(compressedFile) 38 | if err != nil { 39 | cleanup(err) 40 | return 0, err 41 | } 42 | 43 | cleanup(nil) 44 | 45 | if opts.Output != nil { 46 | opts.Output.WriteLine(output.Linef( 47 | output.EmojiLightbulb, 48 | output.StyleItalic, 49 | "Indexed compressed (%.2fMB -> %.2fMB).", 50 | float64(originalSize)/1000/1000, 51 | float64(compressedSize)/1000/1000, 52 | )) 53 | } 54 | 55 | return UploadCompressedIndex(ctx, compressedFile, httpClient, opts, originalSize) 56 | } 57 | 58 | func UploadCompressedIndex(ctx context.Context, compressedFile string, httpClient upload.Client, opts upload.UploadOptions, uncompressedSize int64) (int, error) { 59 | compressedReader, compressedSize, err := openFileAndGetSize(compressedFile) 60 | if err != nil { 61 | // cleanup(err) 62 | return 0, err 63 | } 64 | defer func() { 65 | _ = compressedReader.Close() 66 | }() 67 | 68 | if compressedSize <= opts.MaxPayloadSizeBytes { 69 | return uploadIndex(ctx, httpClient, opts, compressedReader, compressedSize, uncompressedSize) 70 | } 71 | 72 | return uploadMultipartIndex(ctx, httpClient, opts, compressedReader, compressedSize, uncompressedSize) 73 | } 74 | 75 | func getFileSize(filename string) (int64, error) { 76 | fileInfo, err := os.Stat(filename) 77 | if err != nil { 78 | return 1, err 79 | } 80 | 81 | return fileInfo.Size(), nil 82 | } 83 | -------------------------------------------------------------------------------- /cmd/src/codeowners.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | var codeownersCommands commander 11 | 12 | func init() { 13 | usage := `'src codeowners' is a tool that manages ingested code ownership data in a Sourcegraph instance. 14 | 15 | Usage: 16 | 17 | src codeowners command [command options] 18 | 19 | The commands are: 20 | 21 | get returns the codeowners file for a repository, if exists 22 | create create a codeowners file 23 | update update a codeowners file 24 | delete delete a codeowners file 25 | 26 | Use "src codeowners [command] -h" for more information about a command. 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("codeowners", flag.ExitOnError) 30 | handler := func(args []string) error { 31 | codeownersCommands.run(flagSet, "src codeowners", usage, args) 32 | return nil 33 | } 34 | 35 | // Register the command. 36 | commands = append(commands, &command{ 37 | flagSet: flagSet, 38 | aliases: []string{"codeowner"}, 39 | handler: handler, 40 | usageFunc: func() { 41 | fmt.Println(usage) 42 | }, 43 | }) 44 | } 45 | 46 | const codeownersFragment = ` 47 | fragment CodeownersFileFields on CodeownersIngestedFile { 48 | contents 49 | repository { 50 | name 51 | } 52 | } 53 | ` 54 | 55 | type CodeownersIngestedFile struct { 56 | Contents string `json:"contents"` 57 | Repository struct { 58 | Name string `json:"name"` 59 | } `json:"repository"` 60 | } 61 | 62 | func readFile(f string) (io.Reader, error) { 63 | if f == "-" { 64 | return os.Stdin, nil 65 | } 66 | return os.Open(f) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/src/codeowners_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/sourcegraph/sourcegraph/lib/errors" 10 | 11 | "github.com/sourcegraph/src-cli/internal/api" 12 | "github.com/sourcegraph/src-cli/internal/cmderrors" 13 | ) 14 | 15 | func init() { 16 | usage := ` 17 | Examples: 18 | 19 | Delete a codeowners file for the repository "github.com/sourcegraph/sourcegraph": 20 | 21 | $ src codeowners delete -repo='github.com/sourcegraph/sourcegraph' 22 | ` 23 | 24 | flagSet := flag.NewFlagSet("delete", flag.ExitOnError) 25 | usageFunc := func() { 26 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) 27 | flagSet.PrintDefaults() 28 | fmt.Println(usage) 29 | } 30 | var ( 31 | repoFlag = flagSet.String("repo", "", "The repository to delete the data for") 32 | apiFlags = api.NewFlags(flagSet) 33 | ) 34 | 35 | handler := func(args []string) error { 36 | if err := flagSet.Parse(args); err != nil { 37 | return err 38 | } 39 | 40 | if *repoFlag == "" { 41 | return errors.New("provide a repo name using -repo") 42 | } 43 | 44 | client := cfg.apiClient(apiFlags, flagSet.Output()) 45 | 46 | query := `mutation DeleteCodeownersFile( 47 | $repoName: String!, 48 | ) { 49 | deleteCodeownersFiles(repositories: [{ 50 | repoName: $repoName, 51 | }]) { 52 | alwaysNil 53 | } 54 | } 55 | ` 56 | 57 | var result struct { 58 | DeleteCodeownersFile CodeownersIngestedFile 59 | } 60 | if ok, err := client.NewRequest(query, map[string]interface{}{ 61 | "repoName": *repoFlag, 62 | }).Do(context.Background(), &result); err != nil || !ok { 63 | var gqlErr api.GraphQlErrors 64 | if errors.As(err, &gqlErr) { 65 | for _, e := range gqlErr { 66 | if strings.Contains(e.Error(), "repo not found:") { 67 | return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) 68 | } 69 | if strings.Contains(e.Error(), "codeowners file not found:") { 70 | return cmderrors.ExitCode(2, errors.Newf("no data found for repository %q", *repoFlag)) 71 | } 72 | } 73 | } 74 | return err 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // Register the command. 81 | codeownersCommands = append(codeownersCommands, &command{ 82 | flagSet: flagSet, 83 | handler: handler, 84 | usageFunc: usageFunc, 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/src/codeowners_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/sourcegraph/sourcegraph/lib/errors" 10 | 11 | "github.com/sourcegraph/src-cli/internal/api" 12 | "github.com/sourcegraph/src-cli/internal/cmderrors" 13 | ) 14 | 15 | func init() { 16 | usage := ` 17 | Examples: 18 | 19 | Read the current codeowners file for the repository "github.com/sourcegraph/sourcegraph": 20 | 21 | $ src codeowners get -repo='github.com/sourcegraph/sourcegraph' 22 | ` 23 | 24 | flagSet := flag.NewFlagSet("get", flag.ExitOnError) 25 | usageFunc := func() { 26 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src codeowners %s':\n", flagSet.Name()) 27 | flagSet.PrintDefaults() 28 | fmt.Println(usage) 29 | } 30 | var ( 31 | repoFlag = flagSet.String("repo", "", "The repository to attach the data to") 32 | apiFlags = api.NewFlags(flagSet) 33 | ) 34 | 35 | handler := func(args []string) error { 36 | if err := flagSet.Parse(args); err != nil { 37 | return err 38 | } 39 | 40 | if *repoFlag == "" { 41 | return errors.New("provide a repo name using -repo") 42 | } 43 | 44 | client := cfg.apiClient(apiFlags, flagSet.Output()) 45 | 46 | query := `query GetCodeownersFile( 47 | $repoName: String! 48 | ) { 49 | repository(name: $repoName) { 50 | ingestedCodeowners { 51 | ...CodeownersFileFields 52 | } 53 | } 54 | } 55 | ` + codeownersFragment 56 | 57 | var result struct { 58 | Repository *struct { 59 | IngestedCodeowners *CodeownersIngestedFile 60 | } 61 | } 62 | if ok, err := client.NewRequest(query, map[string]interface{}{ 63 | "repoName": *repoFlag, 64 | }).Do(context.Background(), &result); err != nil || !ok { 65 | return err 66 | } 67 | 68 | if result.Repository == nil { 69 | return cmderrors.ExitCode(2, errors.Newf("repository %q not found", *repoFlag)) 70 | } 71 | 72 | if result.Repository.IngestedCodeowners == nil { 73 | return cmderrors.ExitCode(2, errors.Newf("no codeowners data found for %q", *repoFlag)) 74 | } 75 | 76 | fmt.Fprintf(os.Stdout, "%s", result.Repository.IngestedCodeowners.Contents) 77 | 78 | return nil 79 | } 80 | 81 | // Register the command. 82 | codeownersCommands = append(codeownersCommands, &command{ 83 | flagSet: flagSet, 84 | handler: handler, 85 | usageFunc: usageFunc, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/src/debug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var debugCommands commander 9 | 10 | func init() { 11 | usage := `'src debug' gathers and bundles debug data from a Sourcegraph deployment for troubleshooting. 12 | 13 | Usage: 14 | 15 | src debug command [command options] 16 | 17 | The commands are: 18 | 19 | kube dumps context from k8s deployments 20 | compose dumps context from docker-compose deployments 21 | server dumps context from single-container deployments 22 | 23 | 24 | Use "src debug command -h" for more information about a subcommands. 25 | src debug has access to flags on src -- Ex: src -v kube -o foo.zip 26 | 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("debug", flag.ExitOnError) 30 | handler := func(args []string) error { 31 | debugCommands.run(flagSet, "src debug", usage, args) 32 | return nil 33 | } 34 | 35 | // Register the command. 36 | commands = append(commands, &command{ 37 | flagSet: flagSet, 38 | aliases: []string{}, 39 | handler: handler, 40 | usageFunc: func() { fmt.Println(usage) }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/src/extensions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var extensionsCommands commander 9 | 10 | func init() { 11 | usage := `'src extensions' is a tool that manages extensions in the extension registry on a Sourcegraph instance. 12 | 13 | DEPRECATED: We're in the process of removing Sourcegraph extensions with our September release. 14 | Learn more: https://docs.sourcegraph.com/extensions/deprecation 15 | 16 | Usage: 17 | 18 | src extensions command [command options] 19 | 20 | The commands are: 21 | 22 | copy copy an extension from Sourcegraph.com to your private extension registry 23 | publish publish the extension in the current directory 24 | list lists extensions 25 | get gets an extension 26 | delete deletes an extension 27 | 28 | Use "src extensions [command] -h" for more information about a command. 29 | 30 | Alias: "src ext" 31 | ` 32 | 33 | flagSet := flag.NewFlagSet("extensions", flag.ExitOnError) 34 | handler := func(args []string) error { 35 | extensionsCommands.run(flagSet, "src extensions", usage, args) 36 | return nil 37 | } 38 | 39 | // Register the command. 40 | commands = append(commands, &command{ 41 | flagSet: flagSet, 42 | aliases: []string{"ext", "extension"}, 43 | handler: handler, 44 | usageFunc: func() { 45 | fmt.Println(usage) 46 | }, 47 | }) 48 | } 49 | 50 | const registryExtensionFragment = ` 51 | fragment RegistryExtensionFields on RegistryExtension { 52 | id 53 | uuid 54 | extensionID 55 | name 56 | createdAt 57 | updatedAt 58 | url 59 | remoteURL 60 | registryName 61 | isLocal 62 | manifest { 63 | raw 64 | description 65 | bundleURL 66 | } 67 | } 68 | ` 69 | 70 | type Extension struct { 71 | ID string 72 | UUID string 73 | ExtensionID string 74 | Name string 75 | CreatedAt string 76 | UpdatedAt string 77 | URL string 78 | RemoteURL string 79 | RegistryName string 80 | IsLocal bool 81 | Manifest struct { 82 | Raw string 83 | Title string 84 | Description string 85 | BundleURL string 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cmd/src/extensions_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Delete the extension by ID (GraphQL API ID, not extension ID): 16 | 17 | $ src extensions delete -id=UmVnaXN0cnlFeHRlbnNpb246... 18 | 19 | Delete the extension with extension ID "alice/myextension": 20 | 21 | $ src extensions delete -id=$(src extensions get -f '{{.ID}}' -extension-id=alice/myextension) 22 | 23 | ` 24 | 25 | flagSet := flag.NewFlagSet("delete", flag.ExitOnError) 26 | usageFunc := func() { 27 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src extensions %s':\n", flagSet.Name()) 28 | flagSet.PrintDefaults() 29 | fmt.Println(usage) 30 | } 31 | var ( 32 | extensionIDFlag = flagSet.String("id", "", `The ID (GraphQL API ID, not extension ID) of the extension to delete.`) 33 | apiFlags = api.NewFlags(flagSet) 34 | ) 35 | 36 | handler := func(args []string) error { 37 | err := flagSet.Parse(args) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | client := cfg.apiClient(apiFlags, flagSet.Output()) 43 | 44 | query := `mutation DeleteExtension( 45 | $extension: ID! 46 | ) { 47 | extensionRegistry { 48 | deleteExtension( 49 | extension: $extension 50 | ) { 51 | alwaysNil 52 | } 53 | } 54 | }` 55 | 56 | var result struct { 57 | ExtensionRegistry struct { 58 | DeleteExtension struct{} 59 | } 60 | } 61 | if ok, err := client.NewRequest(query, map[string]interface{}{ 62 | "extension": *extensionIDFlag, 63 | }).Do(context.Background(), &result); err != nil || !ok { 64 | return err 65 | } 66 | 67 | fmt.Printf("Extension with ID %q deleted.\n", *extensionIDFlag) 68 | return nil 69 | } 70 | 71 | // Register the command. 72 | extensionsCommands = append(extensionsCommands, &command{ 73 | flagSet: flagSet, 74 | handler: handler, 75 | usageFunc: usageFunc, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/src/extensions_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Get extension with extension ID "alice/myextension": 16 | 17 | $ src extensions get alice/myextension 18 | $ src extensions get -extension-id=alice/myextension 19 | 20 | ` 21 | 22 | flagSet := flag.NewFlagSet("get", flag.ExitOnError) 23 | usageFunc := func() { 24 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src extensions %s':\n", flagSet.Name()) 25 | flagSet.PrintDefaults() 26 | fmt.Println(usage) 27 | } 28 | var ( 29 | extensionIDFlag = flagSet.String("extension-id", "", `Look up extension by extension ID. (e.g. "alice/myextension")`) 30 | formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ExtensionID}}: {{.Manifest.Title}} ({{.RemoteURL}})" or "{{.|json}}")`) 31 | apiFlags = api.NewFlags(flagSet) 32 | ) 33 | 34 | handler := func(args []string) error { 35 | if err := flagSet.Parse(args); err != nil { 36 | return err 37 | } 38 | 39 | tmpl, err := parseTemplate(*formatFlag) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | client := cfg.apiClient(apiFlags, flagSet.Output()) 45 | 46 | query := `query RegistryExtension( 47 | $extensionID: String!, 48 | ) { 49 | extensionRegistry { 50 | extension( 51 | extensionID: $extensionID 52 | ) { 53 | ...RegistryExtensionFields 54 | } 55 | } 56 | }` + registryExtensionFragment 57 | 58 | extensionID := *extensionIDFlag 59 | if extensionID == "" && flagSet.NArg() == 1 { 60 | extensionID = flagSet.Arg(0) 61 | } 62 | 63 | var result struct { 64 | ExtensionRegistry struct { 65 | Extension *Extension 66 | } 67 | } 68 | if ok, err := client.NewRequest(query, map[string]interface{}{ 69 | "extensionID": extensionID, 70 | }).Do(context.Background(), &result); err != nil || !ok { 71 | return err 72 | } 73 | 74 | return execTemplate(tmpl, result.ExtensionRegistry.Extension) 75 | } 76 | 77 | // Register the command. 78 | extensionsCommands = append(extensionsCommands, &command{ 79 | flagSet: flagSet, 80 | handler: handler, 81 | usageFunc: usageFunc, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/src/extensions_publish_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestReadExtensionIDFromManifest(t *testing.T) { 10 | tests := map[string]string{ 11 | `{"name": "a", "publisher": "b"}`: "b/a", 12 | `{"name": "a", "publisher": "b", "extensionID": "c"}`: "c", 13 | `{"extensionID": "c"}`: "c", 14 | } 15 | for manifest, want := range tests { 16 | t.Run(manifest, func(t *testing.T) { 17 | got, err := readExtensionIDFromManifest([]byte(manifest)) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | if got != want { 22 | t.Errorf("got %q, want %q", got, want) 23 | } 24 | }) 25 | } 26 | 27 | t.Run("no name", func(t *testing.T) { 28 | if _, err := readExtensionIDFromManifest([]byte(`{}`)); err == nil { 29 | t.Fatal() 30 | } 31 | }) 32 | 33 | t.Run("no publisher", func(t *testing.T) { 34 | if _, err := readExtensionIDFromManifest([]byte(`{"name":"a"}`)); err == nil { 35 | t.Fatal() 36 | } 37 | }) 38 | } 39 | 40 | func TestUpdatePropertyInManifest(t *testing.T) { 41 | tests := map[string]string{ 42 | `{}`: `{"p": "x"}`, 43 | `{"a":1}`: `{"a":1, "p": "x"}`, 44 | `{"p":"a"}`: `{"p": "x"}`, 45 | } 46 | for manifest, want := range tests { 47 | t.Run(manifest, func(t *testing.T) { 48 | got, err := updatePropertyInManifest([]byte(manifest), "p", "x") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if !jsonDeepEqual(string(got), want) { 53 | t.Errorf("got %q, want %q", got, want) 54 | } 55 | }) 56 | } 57 | 58 | t.Run("remove property", func(t *testing.T) { 59 | manifest := `{"extensionID":"sourcegraph/typescript", "url":"https://sourcegraph.com"}` 60 | want := `{"extensionID": "sourcegraph/typescript"}` 61 | got, err := updatePropertyInManifest([]byte(manifest), "url", "") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if !jsonDeepEqual(string(got), want) { 66 | t.Errorf("got %q, want %q", got, want) 67 | } 68 | }) 69 | } 70 | 71 | func jsonDeepEqual(a, b string) bool { 72 | var va, vb interface{} 73 | if err := json.Unmarshal([]byte(a), &va); err != nil { 74 | panic(err) 75 | } 76 | if err := json.Unmarshal([]byte(b), &vb); err != nil { 77 | panic(err) 78 | } 79 | return reflect.DeepEqual(va, vb) 80 | } 81 | -------------------------------------------------------------------------------- /cmd/src/extsvc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | 10 | "github.com/sourcegraph/src-cli/internal/api" 11 | ) 12 | 13 | var extsvcCommands commander 14 | 15 | func init() { 16 | usage := `'src extsvc' is a tool that manages external services on a Sourcegraph instance. 17 | 18 | Usage: 19 | 20 | src extsvc command [command options] 21 | 22 | The commands are: 23 | 24 | list lists the external services on the Sourcegraph instance 25 | edit edits external services on the Sourcegraph instance 26 | add add an external service on the Sourcegraph instance 27 | 28 | Use "src extsvc [command] -h" for more information about a command. 29 | ` 30 | 31 | flagSet := flag.NewFlagSet("extsvc", flag.ExitOnError) 32 | handler := func(args []string) error { 33 | extsvcCommands.run(flagSet, "src extsvc", usage, args) 34 | return nil 35 | } 36 | 37 | // Register the command. 38 | commands = append(commands, &command{ 39 | flagSet: flagSet, 40 | aliases: []string{"extsvc", "external-service"}, 41 | handler: handler, 42 | usageFunc: func() { 43 | fmt.Println(usage) 44 | }, 45 | }) 46 | } 47 | 48 | type externalService struct { 49 | ID string 50 | Kind string 51 | DisplayName string 52 | Config string 53 | CreatedAt, UpdatedAt string 54 | } 55 | 56 | var errServiceNotFound = errors.New("no such external service") 57 | 58 | func lookupExternalService(ctx context.Context, client api.Client, byID, byName string) (*externalService, error) { 59 | var result struct { 60 | ExternalServices struct { 61 | Nodes []*externalService 62 | } 63 | } 64 | if ok, err := client.NewRequest(externalServicesListQuery, map[string]interface{}{ 65 | "first": 99999, 66 | }).Do(ctx, &result); err != nil || !ok { 67 | return nil, err 68 | } 69 | 70 | for _, svc := range result.ExternalServices.Nodes { 71 | if byID != "" && svc.ID == byID { 72 | return svc, nil 73 | } 74 | if byName != "" && svc.DisplayName == byName { 75 | return svc, nil 76 | } 77 | } 78 | return nil, errServiceNotFound 79 | } 80 | -------------------------------------------------------------------------------- /cmd/src/gateway.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var gatewayCommands commander 9 | 10 | func init() { 11 | usage := `'src gateway' interacts with Cody Gateway (directly or through a Sourcegraph instance). 12 | 13 | Usage: 14 | 15 | src gateway command [command options] 16 | 17 | The commands are: 18 | 19 | benchmark runs benchmarks against Cody Gateway 20 | benchmark-stream runs benchmarks against Cody Gateway code completion streaming endpoints 21 | 22 | Use "src gateway [command] -h" for more information about a command. 23 | 24 | ` 25 | 26 | flagSet := flag.NewFlagSet("gateway", flag.ExitOnError) 27 | handler := func(args []string) error { 28 | gatewayCommands.run(flagSet, "src gateway", usage, args) 29 | return nil 30 | } 31 | 32 | // Register the command. 33 | commands = append(commands, &command{ 34 | flagSet: flagSet, 35 | aliases: []string{}, // No aliases for gateway command 36 | handler: handler, 37 | usageFunc: func() { fmt.Println(usage) }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/src/headers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestParseAdditionalHeaders(t *testing.T) { 11 | testCases := []struct { 12 | environ []string 13 | headers map[string]string 14 | }{ 15 | {environ: []string{}, headers: map[string]string{}}, 16 | {environ: []string{"AUTHORIZATION=foo,bar,baz"}, headers: map[string]string{}}, 17 | {environ: []string{"SRC_HEADER_AUTHORIZATION=foo,bar,baz"}, headers: map[string]string{"authorization": "foo,bar,baz"}}, 18 | {environ: []string{"SRC_HEADER_A=foo", "SRC_HEADER_B=bar", "SRC_HEADER_C=baz"}, headers: map[string]string{"a": "foo", "b": "bar", "c": "baz"}}, 19 | {environ: []string{"SRC_HEADER_A", "SRC_HEADER_B=", "SRC_HEADER_=baz"}, headers: map[string]string{}}, 20 | {environ: []string{"SRC_HEADER_X-Dbx-Auth-Token=foo"}, headers: map[string]string{"x-dbx-auth-token": "foo"}}, 21 | {environ: []string{"SRC_HEADERS=foo:bar\nbar:baz\nAUTHORIZATION:Bearer somerandomstring"}, headers: map[string]string{"foo": "bar", "bar": "baz", "authorization": "Bearer somerandomstring"}}, 22 | {environ: []string{"SRC_HEADERS=foo:bar\nbar:baz\nfoo-bar:baz-bar"}, headers: map[string]string{"foo": "bar", "bar": "baz", "foo-bar": "baz-bar"}}, 23 | {environ: []string{"SRC_HEADERS=\"foo:bar\nbar:baz\nfoo-bar:baz-bar\""}, headers: map[string]string{"foo": "bar", "bar": "baz", "foo-bar": "baz-bar"}}, 24 | {environ: []string{"SRC_HEADERS=foo:bar\nbar:baz\n foo-bar : baz-bar\nb: bar", "SRC_HEADER_A=foo"}, headers: map[string]string{"foo": "bar", "bar": "baz", "foo-bar": "baz-bar", "b": "bar", "a": "foo"}}, 25 | {environ: []string{"SRC_HEADERS", "SRC_HEADER_A=foo"}, headers: map[string]string{"a": "foo"}}, 26 | } 27 | 28 | for _, testCase := range testCases { 29 | t.Run(strings.Join(testCase.environ, " "), func(t *testing.T) { 30 | if diff := cmp.Diff(testCase.headers, parseAdditionalHeadersFromEnviron(testCase.environ)); diff != "" { 31 | t.Errorf("unexpected headers: %s", diff) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/src/orgs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var orgsCommands commander 9 | 10 | func init() { 11 | usage := `'src orgs' is a tool that manages organizations on a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src orgs command [command options] 16 | 17 | The commands are: 18 | 19 | list lists organizations 20 | get gets an organization 21 | create creates an organization 22 | delete deletes an organization 23 | members manages organization members 24 | 25 | Use "src orgs [command] -h" for more information about a command. 26 | ` 27 | 28 | flagSet := flag.NewFlagSet("orgs", flag.ExitOnError) 29 | handler := func(args []string) error { 30 | orgsCommands.run(flagSet, "src orgs", usage, args) 31 | return nil 32 | } 33 | 34 | // Register the command. 35 | commands = append(commands, &command{ 36 | flagSet: flagSet, 37 | aliases: []string{"org"}, 38 | handler: handler, 39 | usageFunc: func() { 40 | fmt.Println(usage) 41 | }, 42 | }) 43 | } 44 | 45 | const orgFragment = ` 46 | fragment OrgFields on Org { 47 | id 48 | name 49 | displayName 50 | members { 51 | nodes { 52 | id 53 | username 54 | } 55 | } 56 | } 57 | ` 58 | 59 | type Org struct { 60 | ID string 61 | Name string 62 | DisplayName string 63 | Members struct { 64 | Nodes []User 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cmd/src/orgs_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Create an organization: 16 | 17 | $ src orgs create -name=abc-org -display-name='ABC Organization' 18 | 19 | ` 20 | 21 | flagSet := flag.NewFlagSet("create", flag.ExitOnError) 22 | usageFunc := func() { 23 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src orgs %s':\n", flagSet.Name()) 24 | flagSet.PrintDefaults() 25 | fmt.Println(usage) 26 | } 27 | var ( 28 | nameFlag = flagSet.String("name", "", `The new organization's name. (required)`) 29 | displayNameFlag = flagSet.String("display-name", "", `The new organization's display name. Defaults to organization name if unspecified.`) 30 | apiFlags = api.NewFlags(flagSet) 31 | ) 32 | 33 | handler := func(args []string) error { 34 | if err := flagSet.Parse(args); err != nil { 35 | return err 36 | } 37 | 38 | client := cfg.apiClient(apiFlags, flagSet.Output()) 39 | 40 | query := `mutation CreateOrg( 41 | $name: String!, 42 | $displayName: String!, 43 | ) { 44 | createOrganization( 45 | name: $name, 46 | displayName: $displayName, 47 | ) { 48 | id 49 | } 50 | }` 51 | 52 | var result struct { 53 | CreateOrg Org 54 | } 55 | if ok, err := client.NewRequest(query, map[string]interface{}{ 56 | "name": *nameFlag, 57 | "displayName": *displayNameFlag, 58 | }).Do(context.Background(), &result); err != nil || !ok { 59 | return err 60 | } 61 | 62 | fmt.Printf("Organization %q created.\n", *nameFlag) 63 | return nil 64 | } 65 | 66 | // Register the command. 67 | orgsCommands = append(orgsCommands, &command{ 68 | flagSet: flagSet, 69 | handler: handler, 70 | usageFunc: usageFunc, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/src/orgs_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Delete an organization by ID: 16 | 17 | $ src orgs delete -id=VXNlcjox 18 | 19 | Delete an organization by name: 20 | 21 | $ src orgs delete -id=$(src orgs get -f='{{.ID}}' -name=abc-org) 22 | 23 | Delete all organizations that match the query 24 | 25 | $ src orgs list -f='{{.ID}}' -query=abc-org | xargs -n 1 -I ORGID src orgs delete -id=ORGID 26 | 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("delete", flag.ExitOnError) 30 | usageFunc := func() { 31 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src orgs %s':\n", flagSet.Name()) 32 | flagSet.PrintDefaults() 33 | fmt.Println(usage) 34 | } 35 | var ( 36 | orgIDFlag = flagSet.String("id", "", `The ID of the organization to delete.`) 37 | apiFlags = api.NewFlags(flagSet) 38 | ) 39 | 40 | handler := func(args []string) error { 41 | if err := flagSet.Parse(args); err != nil { 42 | return err 43 | } 44 | 45 | client := cfg.apiClient(apiFlags, flagSet.Output()) 46 | 47 | query := `mutation DeleteOrganization( 48 | $organization: ID! 49 | ) { 50 | deleteOrganization( 51 | organization: $organization 52 | ) { 53 | alwaysNil 54 | } 55 | }` 56 | 57 | var result struct { 58 | DeleteOrganization struct{} 59 | } 60 | if ok, err := client.NewRequest(query, map[string]interface{}{ 61 | "organization": *orgIDFlag, 62 | }).Do(context.Background(), &result); err != nil || !ok { 63 | return err 64 | } 65 | 66 | fmt.Printf("Organization with ID %q deleted.\n", *orgIDFlag) 67 | return nil 68 | } 69 | 70 | // Register the command. 71 | orgsCommands = append(orgsCommands, &command{ 72 | flagSet: flagSet, 73 | handler: handler, 74 | usageFunc: usageFunc, 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/src/orgs_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Get organization named abc-org: 16 | 17 | $ src orgs get -name=abc-org 18 | 19 | List usernames of members of organization named abc-org (replace '.Username' with '.ID' to list user IDs): 20 | 21 | $ src orgs get -f '{{range $i,$ := .Members.Nodes}}{{if ne $i 0}}{{"\n"}}{{end}}{{.Username}}{{end}}' -name=abc-org 22 | 23 | ` 24 | 25 | flagSet := flag.NewFlagSet("get", flag.ExitOnError) 26 | usageFunc := func() { 27 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src orgs %s':\n", flagSet.Name()) 28 | flagSet.PrintDefaults() 29 | fmt.Println(usage) 30 | } 31 | var ( 32 | nameFlag = flagSet.String("name", "", `Look up organization by name. (e.g. "abc-org")`) 33 | formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}} ({{.DisplayName}})")`) 34 | apiFlags = api.NewFlags(flagSet) 35 | ) 36 | 37 | handler := func(args []string) error { 38 | if err := flagSet.Parse(args); err != nil { 39 | return err 40 | } 41 | 42 | client := cfg.apiClient(apiFlags, flagSet.Output()) 43 | 44 | tmpl, err := parseTemplate(*formatFlag) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | query := `query Organization( 50 | $name: String!, 51 | ) { 52 | organization( 53 | name: $name 54 | ) { 55 | ...OrgFields 56 | } 57 | }` + orgFragment 58 | 59 | var result struct { 60 | Organization *Org 61 | } 62 | if ok, err := client.NewRequest(query, map[string]interface{}{ 63 | "name": *nameFlag, 64 | }).Do(context.Background(), &result); err != nil || !ok { 65 | return err 66 | } 67 | 68 | return execTemplate(tmpl, result.Organization) 69 | } 70 | 71 | // Register the command. 72 | orgsCommands = append(orgsCommands, &command{ 73 | flagSet: flagSet, 74 | handler: handler, 75 | usageFunc: usageFunc, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/src/orgs_members.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var orgsMembersCommands commander 9 | 10 | func init() { 11 | usage := `'src orgs members' is a tool that manages organization members on a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src orgs members command [command options] 16 | 17 | The commands are: 18 | 19 | add adds a user as a member to an organization 20 | remove removes a user as a member from an organization 21 | 22 | Use "src orgs members [command] -h" for more information about a command. 23 | ` 24 | 25 | flagSet := flag.NewFlagSet("members", flag.ExitOnError) 26 | handler := func(args []string) error { 27 | orgsMembersCommands.run(flagSet, "src orgs members", usage, args) 28 | return nil 29 | } 30 | 31 | // Register the command. 32 | orgsCommands = append(orgsCommands, &command{ 33 | flagSet: flagSet, 34 | aliases: []string{"member"}, 35 | handler: handler, 36 | usageFunc: func() { 37 | fmt.Println(usage) 38 | }, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /cmd/src/orgs_members_add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Add a member (alice) to an organization (abc-org): 16 | 17 | $ src orgs members add -org-id=$(src org get -f '{{.ID}}' -name=abc-org) -username=alice 18 | 19 | ` 20 | 21 | flagSet := flag.NewFlagSet("add", flag.ExitOnError) 22 | usageFunc := func() { 23 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src orgs members %s':\n", flagSet.Name()) 24 | flagSet.PrintDefaults() 25 | fmt.Println(usage) 26 | } 27 | var ( 28 | orgIDFlag = flagSet.String("org-id", "", "ID of organization to which to add member. (required)") 29 | usernameFlag = flagSet.String("username", "", "Username of user to add as member. (required)") 30 | apiFlags = api.NewFlags(flagSet) 31 | ) 32 | 33 | handler := func(args []string) error { 34 | if err := flagSet.Parse(args); err != nil { 35 | return err 36 | } 37 | 38 | client := cfg.apiClient(apiFlags, flagSet.Output()) 39 | 40 | query := `mutation AddUserToOrganization( 41 | $organization: ID!, 42 | $username: String!, 43 | ) { 44 | addUserToOrganization( 45 | organization: $organization, 46 | username: $username, 47 | ) { 48 | alwaysNil 49 | } 50 | }` 51 | 52 | var result struct { 53 | AddUserToOrganization struct{} 54 | } 55 | if ok, err := client.NewRequest(query, map[string]interface{}{ 56 | "organization": *orgIDFlag, 57 | "username": *usernameFlag, 58 | }).Do(context.Background(), &result); err != nil || !ok { 59 | return err 60 | } 61 | 62 | fmt.Printf("User %q added as member to organization with ID %q.\n", *usernameFlag, *orgIDFlag) 63 | return nil 64 | } 65 | 66 | // Register the command. 67 | orgsMembersCommands = append(orgsMembersCommands, &command{ 68 | flagSet: flagSet, 69 | handler: handler, 70 | usageFunc: usageFunc, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/src/orgs_members_remove.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Remove a member (alice) from an organization (abc-org): 16 | 17 | $ src orgs members remove -org-id=$(src org get -f '{{.ID}}' -name=abc-org) -user-id=$(src users get -f '{{.ID}}' -username=alice) 18 | ` 19 | 20 | flagSet := flag.NewFlagSet("remove", flag.ExitOnError) 21 | usageFunc := func() { 22 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src orgs members %s':\n", flagSet.Name()) 23 | flagSet.PrintDefaults() 24 | fmt.Println(usage) 25 | } 26 | var ( 27 | orgIDFlag = flagSet.String("org-id", "", "ID of organization from which to remove member. (required)") 28 | userIDFlag = flagSet.String("user-id", "", "ID of user to remove as member. (required)") 29 | apiFlags = api.NewFlags(flagSet) 30 | ) 31 | 32 | handler := func(args []string) error { 33 | if err := flagSet.Parse(args); err != nil { 34 | return err 35 | } 36 | 37 | client := cfg.apiClient(apiFlags, flagSet.Output()) 38 | 39 | query := `mutation RemoveUserFromOrg( 40 | $orgID: ID!, 41 | $userID: ID!, 42 | ) { 43 | removeUserFromOrg( 44 | orgID: $orgID, 45 | userID: $userID, 46 | ) { 47 | alwaysNil 48 | } 49 | }` 50 | 51 | var result struct { 52 | RemoveUserFromOrg struct{} 53 | } 54 | if ok, err := client.NewRequest(query, map[string]interface{}{ 55 | "orgID": *orgIDFlag, 56 | "userID": *userIDFlag, 57 | }).Do(context.Background(), &result); err != nil || !ok { 58 | return err 59 | } 60 | 61 | fmt.Printf("User %q removed as member from organization with ID %q.\n", *userIDFlag, *orgIDFlag) 62 | return nil 63 | } 64 | 65 | // Register the command. 66 | orgsMembersCommands = append(orgsMembersCommands, &command{ 67 | flagSet: flagSet, 68 | handler: handler, 69 | usageFunc: usageFunc, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/src/repos_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | 10 | "github.com/sourcegraph/src-cli/internal/api" 11 | ) 12 | 13 | func init() { 14 | flagSet := flag.NewFlagSet("delete", flag.ExitOnError) 15 | apiFlags := api.NewFlags(flagSet) 16 | 17 | printUsage := func() { 18 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src repos %s'\n", flagSet.Name()) 19 | 20 | flagSet.PrintDefaults() 21 | 22 | examples := ` 23 | Examples: 24 | 25 | Delete one or more repositories: 26 | 27 | $ src repos delete github.com/my/repo github.com/my/repo2 28 | ` 29 | fmt.Fprint(flag.CommandLine.Output(), examples) 30 | } 31 | 32 | deleteRepository := func(ctx context.Context, client api.Client, repoName string) error { 33 | repoID, err := fetchRepositoryID(ctx, client, repoName) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | query := `mutation DeleteRepository($repoID: ID!){ 39 | deleteRepository(repository: $repoID) { 40 | alwaysNil 41 | } 42 | }` 43 | var result struct{} 44 | if ok, err := client.NewRequest(query, map[string]interface{}{ 45 | "repoID": repoID, 46 | }).Do(ctx, &result); err != nil || !ok { 47 | return err 48 | } 49 | 50 | fmt.Fprintf(flag.CommandLine.Output(), "Repository %q deleted\n", repoName) 51 | return nil 52 | } 53 | 54 | deleteRepositories := func(args []string) error { 55 | if err := flagSet.Parse(args); err != nil { 56 | return err 57 | } 58 | 59 | ctx := context.Background() 60 | client := cfg.apiClient(apiFlags, flagSet.Output()) 61 | 62 | var errs errors.MultiError 63 | for _, repoName := range flagSet.Args() { 64 | err := deleteRepository(ctx, client, repoName) 65 | if err != nil { 66 | err = errors.Wrapf(err, "Failed to delete repository %q", repoName) 67 | errs = errors.Append(errs, err) 68 | } 69 | } 70 | return errs 71 | } 72 | 73 | // Register the command. 74 | reposCommands = append(reposCommands, &command{ 75 | flagSet: flagSet, 76 | handler: deleteRepositories, 77 | usageFunc: printUsage, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/src/repos_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Look up a repository by name: 16 | 17 | $ src repos get -name=github.com/sourcegraph/src-cli 18 | 19 | ` 20 | 21 | flagSet := flag.NewFlagSet("get", flag.ExitOnError) 22 | usageFunc := func() { 23 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src repos %s':\n", flagSet.Name()) 24 | flagSet.PrintDefaults() 25 | fmt.Println(usage) 26 | } 27 | var ( 28 | nameFlag = flagSet.String("name", "", "The name of the repository. (required)") 29 | formatFlag = flagSet.String("f", "{{.ID}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}}") or "{{.|json}}")`) 30 | apiFlags = api.NewFlags(flagSet) 31 | ) 32 | 33 | handler := func(args []string) error { 34 | if err := flagSet.Parse(args); err != nil { 35 | return err 36 | } 37 | 38 | client := cfg.apiClient(apiFlags, flagSet.Output()) 39 | 40 | tmpl, err := parseTemplate(*formatFlag) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | query := `query Repository( 46 | $name: String!, 47 | ) { 48 | repository( 49 | name: $name 50 | ) { 51 | ...RepositoryFields 52 | } 53 | } 54 | ` + repositoryFragment 55 | 56 | var result struct { 57 | Repository Repository 58 | } 59 | if ok, err := client.NewRequest(query, map[string]interface{}{ 60 | "name": *nameFlag, 61 | }).Do(context.Background(), &result); err != nil || !ok { 62 | return err 63 | } 64 | 65 | return execTemplate(tmpl, result.Repository) 66 | } 67 | 68 | // Register the command. 69 | reposCommands = append(reposCommands, &command{ 70 | flagSet: flagSet, 71 | handler: handler, 72 | usageFunc: usageFunc, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/src/sbom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var sbomCommands commander 9 | 10 | func init() { 11 | usage := `'src sbom' fetches and verifies SBOM (Software Bill of Materials) data for Sourcegraph containers. 12 | 13 | Usage: 14 | 15 | src sbom command [command options] 16 | 17 | The commands are: 18 | 19 | fetch fetch SBOMs for a released version of Sourcegraph 20 | ` 21 | flagSet := flag.NewFlagSet("sbom", flag.ExitOnError) 22 | handler := func(args []string) error { 23 | sbomCommands.run(flagSet, "src sbom", usage, args) 24 | return nil 25 | } 26 | 27 | // Register the command. 28 | commands = append(commands, &command{ 29 | flagSet: flagSet, 30 | aliases: []string{"sbom"}, 31 | handler: handler, 32 | usageFunc: func() { 33 | fmt.Println(usage) 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/src/scout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var scoutCommands commander 9 | 10 | func init() { 11 | usage := `'src scout' is a tool that provides monitoring for Sourcegraph resources 12 | 13 | EXPERIMENTAL: 'scout' is an experimental command in the 'src' tool. To use, you must 14 | point your .kube config to your Sourcegraph instance. 15 | 16 | Usage: 17 | 18 | src scout command [command options] 19 | 20 | The commands are: 21 | 22 | resource print all known sourcegraph resources and their allocations 23 | usage get CPU, memory and current disk usage 24 | advise recommend lowering or raising resource allocations based on actual usage 25 | 26 | Use "src scout [command] -h" for more information about a command. 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("scout", flag.ExitOnError) 30 | handler := func(args []string) error { 31 | scoutCommands.run(flagSet, "src scout", usage, args) 32 | return nil 33 | } 34 | 35 | commands = append(commands, &command{ 36 | flagSet: flagSet, 37 | aliases: []string{"scout"}, 38 | handler: handler, 39 | usageFunc: func() { 40 | fmt.Println(usage) 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/src/search_jobs_cancel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | // GraphQL mutation constants 12 | const cancelSearchJobMutation = `mutation CancelSearchJob($id: ID!) { 13 | cancelSearchJob(id: $id) { 14 | alwaysNil 15 | } 16 | }` 17 | 18 | // cancelSearchJob cancels a search job with the given ID 19 | func cancelSearchJob(client api.Client, jobID string) error { 20 | var result struct { 21 | CancelSearchJob struct { 22 | AlwaysNil bool 23 | } 24 | } 25 | 26 | if ok, err := client.NewRequest(cancelSearchJobMutation, map[string]interface{}{ 27 | "id": jobID, 28 | }).Do(context.Background(), &result); err != nil || !ok { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // displayCancelSuccessMessage outputs a success message for the canceled job 36 | func displayCancelSuccessMessage(out *flag.FlagSet, jobID string) { 37 | fmt.Fprintf(out.Output(), "Search job %s canceled successfully\n", jobID) 38 | } 39 | 40 | // init registers the 'cancel' subcommand for search jobs, which allows users to cancel 41 | // a running search job by its ID. It sets up the command's flag parsing, usage information, 42 | // and handles the GraphQL mutation to cancel the specified search job. 43 | func init() { 44 | usage := ` 45 | Examples: 46 | 47 | Cancel a search job by ID: 48 | 49 | $ src search-jobs cancel U2VhcmNoSm9iOjY5 50 | 51 | Arguments: 52 | The ID of the search job to cancel. 53 | 54 | The cancel command stops a running search job and outputs a confirmation message. 55 | ` 56 | 57 | cmd := newSearchJobCommand("cancel", usage) 58 | 59 | cmd.build(func(flagSet *flag.FlagSet, apiFlags *api.Flags, columns []string, asJSON bool, client api.Client) error { 60 | 61 | jobID, err := validateJobID(flagSet.Args()) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if err := cancelSearchJob(client, jobID); err != nil { 67 | return err 68 | } 69 | 70 | if apiFlags.GetCurl() { 71 | return nil 72 | } 73 | 74 | displayCancelSuccessMessage(flagSet, jobID) 75 | 76 | return nil 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/src/search_jobs_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | // GraphQL mutation constants 12 | const deleteSearchJobQuery = `mutation DeleteSearchJob($id: ID!) { 13 | deleteSearchJob(id: $id) { 14 | alwaysNil 15 | } 16 | }` 17 | 18 | // deleteSearchJob deletes a search job with the given ID 19 | func deleteSearchJob(client api.Client, jobID string) error { 20 | var result struct { 21 | DeleteSearchJob struct { 22 | AlwaysNil bool 23 | } 24 | } 25 | 26 | if ok, err := client.NewRequest(deleteSearchJobQuery, map[string]interface{}{ 27 | "id": jobID, 28 | }).Do(context.Background(), &result); err != nil || !ok { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // displaySuccessMessage outputs a success message for the deleted job 36 | func displaySuccessMessage(out *flag.FlagSet, jobID string) { 37 | fmt.Fprintf(out.Output(), "Search job %s deleted successfully\n", jobID) 38 | } 39 | 40 | // init registers the 'delete' subcommand for search-jobs which allows users to delete 41 | // a search job by its ID. The command requires a search job ID to be provided via 42 | // the -id flag and will make a GraphQL mutation to delete the specified job. 43 | func init() { 44 | usage := ` 45 | Examples: 46 | 47 | Delete a search job by ID: 48 | 49 | $ src search-jobs delete U2VhcmNoSm9iOjY5 50 | 51 | Arguments: 52 | The ID of the search job to delete. 53 | 54 | The delete command permanently removes a search job and outputs a confirmation message. 55 | ` 56 | 57 | cmd := newSearchJobCommand("delete", usage) 58 | 59 | cmd.build(func(flagSet *flag.FlagSet, apiFlags *api.Flags, columns []string, asJSON bool, client api.Client) error { 60 | 61 | jobID, err := validateJobID(flagSet.Args()) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if err := deleteSearchJob(client, jobID); err != nil { 67 | return err 68 | } 69 | 70 | if apiFlags.GetCurl() { 71 | return nil 72 | } 73 | 74 | displaySuccessMessage(flagSet, jobID) 75 | return nil 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/src/search_jobs_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "github.com/sourcegraph/src-cli/internal/api" 8 | ) 9 | 10 | // GraphQL query constants 11 | const getSearchJobQuery = `query SearchJob($id: ID!) { 12 | node(id: $id) { 13 | ... on SearchJob { 14 | ...SearchJobFields 15 | } 16 | } 17 | } 18 | ` 19 | 20 | // getSearchJob fetches a search job by ID 21 | func getSearchJob(client api.Client, id string) (*SearchJob, error) { 22 | query := getSearchJobQuery + searchJobFragment 23 | 24 | var result struct { 25 | Node *SearchJob 26 | } 27 | 28 | if ok, err := client.NewRequest(query, map[string]interface{}{ 29 | "id": api.NullString(id), 30 | }).Do(context.Background(), &result); err != nil || !ok { 31 | return nil, err 32 | } 33 | 34 | return result.Node, nil 35 | } 36 | 37 | // init registers the "get" subcommand for search-jobs 38 | func init() { 39 | usage := ` 40 | Examples: 41 | 42 | Get a search job by ID: 43 | 44 | $ src search-jobs get U2VhcmNoSm9iOjY5 45 | 46 | Get a search job with specific columns: 47 | 48 | $ src search-jobs get U2VhcmNoSm9iOjY5 -c id,state,username 49 | 50 | Get a search job in JSON format: 51 | 52 | $ src search-jobs get U2VhcmNoSm9iOjY5 -json 53 | 54 | Available columns are: id, query, state, username, createdat, startedat, finishedat, 55 | url, logurl, total, completed, failed, inprogress 56 | ` 57 | 58 | cmd := newSearchJobCommand("get", usage) 59 | 60 | cmd.build(func(flagSet *flag.FlagSet, apiFlags *api.Flags, columns []string, asJSON bool, client api.Client) error { 61 | 62 | id, err := validateJobID(flagSet.Args()) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | job, err := getSearchJob(client, id) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if apiFlags.GetCurl() { 73 | return nil 74 | } 75 | 76 | // Display the job with selected columns or as JSON 77 | return displaySearchJob(job, columns, asJSON) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/src/search_jobs_restart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/sourcegraph/src-cli/internal/api" 8 | "github.com/sourcegraph/src-cli/internal/cmderrors" 9 | ) 10 | 11 | // restartSearchJob restarts a search job with the same query as the original 12 | func restartSearchJob(client api.Client, jobID string) (*SearchJob, error) { 13 | originalJob, err := getSearchJob(client, jobID) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if originalJob == nil { 19 | return nil, fmt.Errorf("no job found with ID %s", jobID) 20 | } 21 | 22 | query := originalJob.Query 23 | 24 | return createSearchJob(client, query) 25 | } 26 | 27 | // init registers the "restart" subcommand for search jobs, which allows restarting 28 | // a search job by its ID. It sets up command-line flags for job ID and output formatting, 29 | // validates the search job query, and creates a new search job with the same query 30 | // as the original job. 31 | func init() { 32 | usage := ` 33 | Examples: 34 | 35 | Restart a search job by ID: 36 | 37 | $ src search-jobs restart U2VhcmNoSm9iOjY5 38 | 39 | Restart a search job and display specific columns: 40 | 41 | $ src search-jobs restart U2VhcmNoSm9iOjY5 -c id,state,query 42 | 43 | Restart a search job and output in JSON format: 44 | 45 | $ src search-jobs restart U2VhcmNoSm9iOjY5 -json 46 | 47 | Available columns are: id, query, state, username, createdat, startedat, finishedat, 48 | url, logurl, total, completed, failed, inprogress 49 | ` 50 | 51 | cmd := newSearchJobCommand("restart", usage) 52 | 53 | cmd.build(func(flagSet *flag.FlagSet, apiFlags *api.Flags, columns []string, asJSON bool, client api.Client) error { 54 | if flagSet.NArg() != 1 { 55 | return cmderrors.Usage("must provide a job ID") 56 | } 57 | jobID := flagSet.Arg(0) 58 | 59 | newJob, err := restartSearchJob(client, jobID) 60 | 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if apiFlags.GetCurl() { 66 | return nil 67 | } 68 | 69 | return displaySearchJob(newJob, columns, asJSON) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/src/servegit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/sourcegraph/src-cli/internal/cmderrors" 11 | "github.com/sourcegraph/src-cli/internal/servegit" 12 | ) 13 | 14 | func init() { 15 | flagSet := flag.NewFlagSet("serve-git", flag.ExitOnError) 16 | usageFunc := func() { 17 | fmt.Fprintf(flag.CommandLine.Output(), `'src serve-git' serves your local git repositories over HTTP for Sourcegraph to pull. 18 | 19 | USAGE 20 | src [-v] serve-git [-list] [-addr :3434] [path/to/dir] 21 | 22 | By default 'src serve-git' will recursively serve your current directory on the address ':3434'. 23 | 24 | 'src serve-git -list' will not start up the server. Instead it will write to stdout a list of 25 | repository names it would serve. 26 | 27 | Documentation at https://sourcegraph.com/docs/admin/code_hosts/src_serve_git 28 | `) 29 | } 30 | var ( 31 | addrFlag = flagSet.String("addr", ":3434", "Address on which to serve (end with : for unused port)") 32 | listFlag = flagSet.Bool("list", false, "list found repository names") 33 | ) 34 | 35 | handler := func(args []string) error { 36 | err := flagSet.Parse(args) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | var repoDir string 42 | switch args := flagSet.Args(); len(args) { 43 | case 0: 44 | repoDir, err = os.Getwd() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | case 1: 50 | repoDir = args[0] 51 | 52 | default: 53 | return cmderrors.Usage("requires zero or one arguments") 54 | } 55 | 56 | dbug := log.New(io.Discard, "", log.LstdFlags) 57 | if *verbose { 58 | dbug = log.New(os.Stderr, "DBUG serve-git: ", log.LstdFlags) 59 | } 60 | 61 | s := &servegit.Serve{ 62 | Addr: *addrFlag, 63 | Root: repoDir, 64 | Info: log.New(os.Stderr, "serve-git: ", log.LstdFlags), 65 | Debug: dbug, 66 | } 67 | 68 | if *listFlag { 69 | repos, err := s.Repos() 70 | if err != nil { 71 | return err 72 | } 73 | for _, r := range repos { 74 | fmt.Println(r.Name) 75 | } 76 | return nil 77 | } 78 | 79 | return s.Start() 80 | } 81 | 82 | // Register the command. 83 | commands = append(commands, &command{ 84 | aliases: []string{"servegit"}, 85 | flagSet: flagSet, 86 | handler: handler, 87 | usageFunc: usageFunc, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/src/signature.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var signatureCommands commander 9 | 10 | func init() { 11 | usage := `'src signature' verifies published signatures for Sourcegraph containers. 12 | 13 | Usage: 14 | 15 | src signature command [command options] 16 | 17 | The commands are: 18 | 19 | verify verify signatures for a Sourcegraph release 20 | ` 21 | flagSet := flag.NewFlagSet("signature", flag.ExitOnError) 22 | handler := func(args []string) error { 23 | signatureCommands.run(flagSet, "src signature", usage, args) 24 | return nil 25 | } 26 | 27 | // Register the command. 28 | commands = append(commands, &command{ 29 | flagSet: flagSet, 30 | aliases: []string{"signature", "sig"}, 31 | handler: handler, 32 | usageFunc: func() { 33 | fmt.Println(usage) 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/src/snapshot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var snapshotCommands commander 9 | 10 | func init() { 11 | usage := `'src snapshot' manages snapshots of Sourcegraph instance data. All subcommands are currently EXPERIMENTAL. 12 | 13 | USAGE 14 | src [-v] snapshot 15 | 16 | COMMANDS 17 | 18 | summary export summary data about an instance for acceptance testing of a restored Sourcegraph instance 19 | test use exported summary data and instance health indicators to validate a restored and upgraded instance 20 | ` 21 | flagSet := flag.NewFlagSet("snapshot", flag.ExitOnError) 22 | 23 | commands = append(commands, &command{ 24 | flagSet: flagSet, 25 | handler: func(args []string) error { 26 | snapshotCommands.run(flagSet, "src snapshot", usage, args) 27 | return nil 28 | }, 29 | usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/src/teams.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var teamsCommands commander 9 | 10 | func init() { 11 | usage := `'src teams' is a tool that manages teams in a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src teams command [command options] 16 | 17 | The commands are: 18 | 19 | list lists teams 20 | create create a team 21 | update update a team 22 | delete delete a team 23 | members manage team members, use "src teams members [command] -h" for more information. 24 | 25 | Use "src teams [command] -h" for more information about a command. 26 | ` 27 | 28 | flagSet := flag.NewFlagSet("teams", flag.ExitOnError) 29 | handler := func(args []string) error { 30 | teamsCommands.run(flagSet, "src teams", usage, args) 31 | return nil 32 | } 33 | 34 | // Register the command. 35 | commands = append(commands, &command{ 36 | flagSet: flagSet, 37 | aliases: []string{"team"}, 38 | handler: handler, 39 | usageFunc: func() { 40 | fmt.Println(usage) 41 | }, 42 | }) 43 | } 44 | 45 | const teamFragment = ` 46 | fragment TeamFields on Team { 47 | id 48 | name 49 | displayName 50 | readonly 51 | } 52 | ` 53 | 54 | type Team struct { 55 | ID string `json:"id"` 56 | Name string `json:"name"` 57 | DisplayName string `json:"displayName"` 58 | Readonly bool `json:"readonly"` 59 | } 60 | -------------------------------------------------------------------------------- /cmd/src/teams_delete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | 10 | "github.com/sourcegraph/src-cli/internal/api" 11 | ) 12 | 13 | func init() { 14 | usage := ` 15 | Examples: 16 | 17 | Delete the team "engineering": 18 | 19 | $ src teams delete -name='engineering' 20 | 21 | ` 22 | 23 | flagSet := flag.NewFlagSet("delete", flag.ExitOnError) 24 | usageFunc := func() { 25 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src teams %s':\n", flagSet.Name()) 26 | flagSet.PrintDefaults() 27 | fmt.Println(usage) 28 | } 29 | var ( 30 | nameFlag = flagSet.String("name", "", "The team name") 31 | apiFlags = api.NewFlags(flagSet) 32 | ) 33 | 34 | handler := func(args []string) error { 35 | if err := flagSet.Parse(args); err != nil { 36 | return err 37 | } 38 | 39 | if *nameFlag == "" { 40 | return errors.New("provide a name") 41 | } 42 | 43 | client := cfg.apiClient(apiFlags, flagSet.Output()) 44 | 45 | query := `mutation DeleteTeam( 46 | $name: String!, 47 | ) { 48 | deleteTeam( 49 | name: $name, 50 | ) { 51 | alwaysNil 52 | } 53 | } 54 | ` 55 | 56 | var result struct { 57 | DeleteTeam any 58 | } 59 | if ok, err := client.NewRequest(query, map[string]interface{}{ 60 | "name": *nameFlag, 61 | }).Do(context.Background(), &result); err != nil || !ok { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // Register the command. 69 | teamsCommands = append(teamsCommands, &command{ 70 | flagSet: flagSet, 71 | handler: handler, 72 | usageFunc: usageFunc, 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/src/teams_members.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var teamMembersCommands commander 9 | 10 | func init() { 11 | usage := `'src teams members' is a tool that manages team membership in a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src team members command [command options] 16 | 17 | The commands are: 18 | 19 | list lists team members 20 | add add team members 21 | remove remove team members 22 | 23 | Use "src team members [command] -h" for more information about a command. 24 | ` 25 | 26 | flagSet := flag.NewFlagSet("members", flag.ExitOnError) 27 | handler := func(args []string) error { 28 | teamMembersCommands.run(flagSet, "src teams members", usage, args) 29 | return nil 30 | } 31 | 32 | // Register the command. 33 | teamsCommands = append(teamsCommands, &command{ 34 | flagSet: flagSet, 35 | aliases: []string{"member"}, 36 | handler: handler, 37 | usageFunc: func() { 38 | fmt.Println(usage) 39 | }, 40 | }) 41 | } 42 | 43 | const teamMemberFragment = ` 44 | fragment TeamMemberFields on TeamMember { 45 | ... on User { 46 | id 47 | username 48 | } 49 | } 50 | ` 51 | 52 | type TeamMember struct { 53 | ID string `json:"id"` 54 | Username string `json:"username"` 55 | } 56 | -------------------------------------------------------------------------------- /cmd/src/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cleanup() { 4 | src orgs members remove -org-id=$(src org get -f '{{.ID}}' -name=abc-org) -user-id=$(src users get -f '{{.ID}}' -username=alice) 5 | src orgs delete -id=$(src orgs get -f '{{.ID}}' -name=abc-org) 6 | src users delete -id=$(src users get -f='{{.ID}}' -username=alice) 7 | } 8 | 9 | cleanup > /dev/null 2>&1 10 | 11 | set -euf -o pipefail 12 | 13 | unset CDPATH 14 | cd "$(dirname "${BASH_SOURCE[0]}")/../.." # cd to repo root dir 15 | 16 | go install ./cmd/src 17 | 18 | src users create -username=alice -email=alice@example.com 19 | src orgs create -name=abc-org 20 | src orgs members add -org-id=$(src org get -f '{{.ID}}' -name=abc-org) -username=alice 21 | cleanup 22 | 23 | -------------------------------------------------------------------------------- /cmd/src/testdata/TestSearchStream/JSON.golden: -------------------------------------------------------------------------------- 1 | {"type":"content","path":"path/to/file","repository":"org/repo","chunkMatches":[{"content":"foo bar foo","contentStart":{"offset":0,"line":4,"column":0},"ranges":[{"start":{"offset":0,"line":0,"column":0},"end":{"offset":3,"line":0,"column":0}},{"start":{"offset":0,"line":0,"column":0},"end":{"offset":3,"line":0,"column":0}},{"start":{"offset":1,"line":0,"column":0},"end":{"offset":2,"line":0,"column":0}},{"start":{"offset":1,"line":0,"column":0},"end":{"offset":3,"line":0,"column":0}},{"start":{"offset":8,"line":0,"column":0},"end":{"offset":11,"line":0,"column":0}}]}]} 2 | {"type":"repo","repository":"sourcegraph/sourcegraph"} 3 | {"type":"symbol","path":"path/to/file","repository":"org/repo","symbols":[{"url":"github.com/sourcegraph/sourcegraph/-/blob/cmd/frontend/graphqlbackend/search_results.go#L1591:26-1591:35","name":"doResults","containerName":"","kind":"FUNCTION"},{"url":"github.com/sourcegraph/sourcegraph/-/blob/cmd/frontend/graphqlbackend/search_results.go#L1591:26-1591:35","name":"Results","containerName":"SearchResultsResolver","kind":"FIELD"}]} 4 | {"type":"commit","icon":"","label":"[sourcegraph/sourcegraph-atom](/github.com/sourcegraph/sourcegraph-atom) › [Stephen Gutekanst](/github.com/sourcegraph/sourcegraph-atom/-/commit/5b098d7fed963d88e23057ed99d73d3c7a33ad89): [all: release v1.0.5](/github.com/sourcegraph/sourcegraph-atom/-/commit/5b098d7fed963d88e23057ed99d73d3c7a33ad89)^","url":"","detail":"","content":"```COMMIT_EDITMSG\nfoo bar\n```","ranges":[[1,3,3]]} 5 | {"type":"commit","icon":"","label":"[sourcegraph/sourcegraph-atom](/github.com/sourcegraph/sourcegraph-atom) › [Stephen Gutekanst](/github.com/sourcegraph/sourcegraph-atom/-/commit/5b098d7fed963d88e23057ed99d73d3c7a33ad89): [all: release v1.0.5](/github.com/sourcegraph/sourcegraph-atom/-/commit/5b098d7fed963d88e23057ed99d73d3c7a33ad89)^","url":"","detail":"","content":"```diff\nsrc/data.ts src/data.ts\n@@ -0,0 +11,4 @@\n+ return of\u003cData\u003e({\n+ title: 'Acme Corp open-source code search',\n+ summary: 'Instant code search across all Acme Corp open-source code.',\n+ githubOrgs: ['sourcegraph'],\n```","ranges":[[4,44,6]]} 6 | -------------------------------------------------------------------------------- /cmd/src/testdata/TestSearchStream/Text.golden: -------------------------------------------------------------------------------- 1 | org/repo › path/to/file (5 matches) 2 | -------------------------------------------------------------------------------- 3 |   5 | foo bar foo 4 | ------------------------------------------------------------------------------ 5 | sourcegraph/sourcegraph (http://127.0.0.1:55128/sourcegraph/sourcegraph) (1 match) 6 | org/repo › path/to/file (2 matches) 7 | -------------------------------------------------------------------------------- 8 | doResults(FUNCTION) (http://127.0.0.1:55128/github.com/sourcegraph/sourcegraph/-/blob/cmd/frontend/graphqlbackend/search_results.go#L1591:26-1591:35) 9 | Results(FIELD, SearchResultsResolver) (http://127.0.0.1:55128/github.com/sourcegraph/sourcegraph/-/blob/cmd/frontend/graphqlbackend/search_results.go#L1591:26-1591:35) 10 |  11 | (http://127.0.0.1:55128) 12 | sourcegraph/sourcegraph-atom > Stephen Gutekanst : all: release v1.0.5 (1 match) 13 | -------------------------------------------------------------------------------- 14 |  foo bar 15 | 16 | (http://127.0.0.1:55128) 17 | sourcegraph/sourcegraph-atom > Stephen Gutekanst : all: release v1.0.5 (1 match) 18 | -------------------------------------------------------------------------------- 19 |  src/data.ts src/data.ts 20 | @@ -0,0 +11,4 @@ 21 | + return of({ 22 | + title: 'Acme Corp open-source code search', 23 | + summary: 'Instant code search across all Acme Corp open-source code.', 24 | + githubOrgs: ['sourcegraph'], 25 | 26 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/basic-repo-new.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "SourcegraphEndpoint": "https://sourcegraph.com", 3 | "Query": "repogroup:goteam type:repo golang max:4", 4 | "Site": { "BuildVersion": "aaaaa_2018-12-12_aaaaa" }, 5 | "Results": [ 6 | { 7 | "__typename": "Repository", 8 | "name": "github.com/golang/build", 9 | "url": "/github.com/golang/build", 10 | "externalURLs": [ 11 | { 12 | "serviceType": "github", 13 | "url": "https://github.com/golang/build" 14 | } 15 | ], 16 | "label": { 17 | "html": "

github.com/golang/build

\n" 18 | } 19 | }, 20 | { 21 | "__typename": "Repository", 22 | "name": "github.com/golang/crypto", 23 | "url": "/github.com/golang/crypto", 24 | "externalURLs": [ 25 | { 26 | "serviceType": "github", 27 | "url": "https://github.com/golang/crypto" 28 | } 29 | ], 30 | "label": { 31 | "html": "

github.com/golang/crypto

\n" 32 | } 33 | }, 34 | { 35 | "__typename": "Repository", 36 | "name": "github.com/golang/net", 37 | "url": "/github.com/golang/net", 38 | "externalURLs": [ 39 | { 40 | "serviceType": "github", 41 | "url": "https://github.com/golang/net" 42 | } 43 | ], 44 | "label": { 45 | "html": "

github.com/golang/net

\n" 46 | } 47 | }, 48 | { 49 | "__typename": "Repository", 50 | "name": "github.com/golang/protobuf", 51 | "url": "/github.com/golang/protobuf", 52 | "externalURLs": [ 53 | { 54 | "serviceType": "github", 55 | "url": "https://github.com/golang/protobuf" 56 | } 57 | ], 58 | "label": { 59 | "html": "

github.com/golang/protobuf

\n" 60 | } 61 | } 62 | ], 63 | "LimitHit": true, 64 | "Cloning": [], 65 | "Missing": [], 66 | "Timedout": [], 67 | "ResultCount": 4, 68 | "ElapsedMilliseconds": 37 69 | } 70 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/basic-repo-new.want.txt: -------------------------------------------------------------------------------- 1 | ✱ 4+ results for "repogroup:goteam type:repo golang max:4" in 37ms 2 | github.com/golang/build  (https://sourcegraph.com/github.com/golang/build) 3 | github.com/golang/crypto  (https://sourcegraph.com/github.com/golang/crypto) 4 | github.com/golang/net  (https://sourcegraph.com/github.com/golang/net) 5 | github.com/golang/protobuf (https://sourcegraph.com/github.com/golang/protobuf) 6 |  -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/basic-repo.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "SourcegraphEndpoint": "https://sourcegraph.com", 3 | "Query": "repogroup:goteam type:repo golang max:4", 4 | "Results": [ 5 | { 6 | "__typename": "Repository", 7 | "externalURLs": [ 8 | { 9 | "serviceType": "github", 10 | "url": "https://github.com/golang/build" 11 | } 12 | ], 13 | "name": "github.com/golang/build", 14 | "url": "/github.com/golang/build" 15 | }, 16 | { 17 | "__typename": "Repository", 18 | "externalURLs": [ 19 | { 20 | "serviceType": "github", 21 | "url": "https://github.com/golang/crypto" 22 | } 23 | ], 24 | "name": "github.com/golang/crypto", 25 | "url": "/github.com/golang/crypto" 26 | }, 27 | { 28 | "__typename": "Repository", 29 | "externalURLs": [ 30 | { 31 | "serviceType": "github", 32 | "url": "https://github.com/golang/net" 33 | } 34 | ], 35 | "name": "github.com/golang/net", 36 | "url": "/github.com/golang/net" 37 | }, 38 | { 39 | "__typename": "Repository", 40 | "externalURLs": [ 41 | { 42 | "serviceType": "github", 43 | "url": "https://github.com/golang/protobuf" 44 | } 45 | ], 46 | "name": "github.com/golang/protobuf", 47 | "url": "/github.com/golang/protobuf" 48 | } 49 | ], 50 | "LimitHit": true, 51 | "Cloning": [], 52 | "Missing": [], 53 | "Timedout": [], 54 | "ResultCount": 4, 55 | "ElapsedMilliseconds": 37 56 | } 57 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/basic-repo.want.txt: -------------------------------------------------------------------------------- 1 | ✱ 4+ results for "repogroup:goteam type:repo golang max:4" in 37ms 2 | github.com/golang/build  (https://sourcegraph.com/github.com/golang/build) 3 | github.com/golang/crypto  (https://sourcegraph.com/github.com/golang/crypto) 4 | github.com/golang/net  (https://sourcegraph.com/github.com/golang/net) 5 | github.com/golang/protobuf (https://sourcegraph.com/github.com/golang/protobuf) 6 |  -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_missing_timedout.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Query": "repo:AlwaysCloning error", 3 | "Results": [], 4 | "LimitHit": false, 5 | "Cloning": [ 6 | { 7 | "name": "github.com/sourcegraphtest/AlwaysCloningTest" 8 | }, 9 | { 10 | "name": "github.com/sourcegraphtest/AlwaysCloningTest2" 11 | } 12 | ], 13 | "Missing": [ 14 | { 15 | "name": "github.com/sourcegraphtest/AlwaysMissingTest" 16 | }, 17 | { 18 | "name": "github.com/sourcegraphtest/AlwaysMissingTest2" 19 | } 20 | ], 21 | "Timedout": [ 22 | { 23 | "name": "github.com/sourcegraphtest/AlwaysTimedoutTest" 24 | }, 25 | { 26 | "name": "github.com/sourcegraphtest/AlwaysTimedoutTest2" 27 | } 28 | ], 29 | "ResultCount": 0, 30 | "ApproximateResultCount": "", 31 | "ElapsedMilliseconds": 19 32 | } 33 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_missing_timedout.want.txt: -------------------------------------------------------------------------------- 1 | ✱ 0 results for "repo:AlwaysCloning error" in 19ms 2 | (2) still cloning: github.com/sourcegraphtest/AlwaysCloningTest, github.com/sourcegraphtest/AlwaysCloningTest2 3 | (2) missing: github.com/sourcegraphtest/AlwaysMissingTest, github.com/sourcegraphtest/AlwaysMissingTest2 4 | (2) timed out: github.com/sourcegraphtest/AlwaysTimedoutTest, github.com/sourcegraphtest/AlwaysTimedoutTest2 5 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_multiple_repo.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Query": "repo:AlwaysCloning error", 3 | "Results": [], 4 | "LimitHit": false, 5 | "Cloning": [ 6 | { 7 | "name": "github.com/sourcegraphtest/AlwaysCloningTest" 8 | }, 9 | { 10 | "name": "github.com/sourcegraphtest/AlwaysCloningTest2" 11 | } 12 | ], 13 | "Missing": [], 14 | "Timedout": [], 15 | "ResultCount": 0, 16 | "ApproximateResultCount": "", 17 | "ElapsedMilliseconds": 19 18 | } 19 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_multiple_repo.want.txt: -------------------------------------------------------------------------------- 1 | ✱ 0 results for "repo:AlwaysCloning error" in 19ms 2 | (2) still cloning: github.com/sourcegraphtest/AlwaysCloningTest, github.com/sourcegraphtest/AlwaysCloningTest2 3 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_repo.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Query": "repo:AlwaysCloning error", 3 | "Results": [], 4 | "LimitHit": false, 5 | "Cloning": [ 6 | { 7 | "name": "github.com/sourcegraphtest/AlwaysCloningTest" 8 | } 9 | ], 10 | "Missing": [], 11 | "Timedout": [], 12 | "ResultCount": 0, 13 | "ApproximateResultCount": "", 14 | "ElapsedMilliseconds": 19 15 | } 16 | -------------------------------------------------------------------------------- /cmd/src/testdata/search_formatting/cloning_repo.want.txt: -------------------------------------------------------------------------------- 1 | ✱ 0 results for "repo:AlwaysCloning error" in 19ms 2 | (1) still cloning: github.com/sourcegraphtest/AlwaysCloningTest 3 | -------------------------------------------------------------------------------- /cmd/src/users.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var usersCommands commander 9 | 10 | func init() { 11 | usage := `'src users' is a tool that manages users on a Sourcegraph instance. 12 | 13 | Usage: 14 | 15 | src users command [command options] 16 | 17 | The commands are: 18 | 19 | list lists users 20 | get gets a user 21 | create creates a user account 22 | delete deletes a user account 23 | prune deletes inactive users 24 | tag add/remove a tag on a user 25 | 26 | Use "src users [command] -h" for more information about a command. 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("users", flag.ExitOnError) 30 | handler := func(args []string) error { 31 | usersCommands.run(flagSet, "src users", usage, args) 32 | return nil 33 | } 34 | 35 | // Register the command. 36 | commands = append(commands, &command{ 37 | flagSet: flagSet, 38 | aliases: []string{"user"}, 39 | handler: handler, 40 | usageFunc: func() { 41 | fmt.Println(usage) 42 | }, 43 | }) 44 | } 45 | 46 | const userFragment = ` 47 | fragment UserFields on User { 48 | id 49 | username 50 | displayName 51 | siteAdmin 52 | organizations { 53 | nodes { 54 | id 55 | name 56 | displayName 57 | } 58 | } 59 | emails { 60 | email 61 | verified 62 | } 63 | usageStatistics { 64 | lastActiveTime 65 | lastActiveCodeHostIntegrationTime 66 | } 67 | url 68 | } 69 | ` 70 | 71 | type User struct { 72 | ID string 73 | Username string 74 | DisplayName string 75 | SiteAdmin bool 76 | Organizations struct { 77 | Nodes []Org 78 | } 79 | Emails []UserEmail 80 | UsageStatistics UserUsageStatistics 81 | URL string 82 | } 83 | 84 | type UserEmail struct { 85 | Email string 86 | Verified bool 87 | } 88 | 89 | type UserUsageStatistics struct { 90 | LastActiveTime string 91 | LastActiveCodeHostIntegrationTime string 92 | } 93 | 94 | type SiteUser struct { 95 | ID string 96 | Username string 97 | Email string 98 | SiteAdmin bool 99 | LastActiveAt string 100 | DeletedAt string 101 | } 102 | -------------------------------------------------------------------------------- /cmd/src/users_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Create a user account: 16 | 17 | $ src users create -username=alice -email=alice@example.com 18 | 19 | ` 20 | 21 | flagSet := flag.NewFlagSet("create", flag.ExitOnError) 22 | usageFunc := func() { 23 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) 24 | flagSet.PrintDefaults() 25 | fmt.Println(usage) 26 | } 27 | var ( 28 | usernameFlag = flagSet.String("username", "", `The new user's username. (required)`) 29 | emailFlag = flagSet.String("email", "", `The new user's email address. (required)`) 30 | resetPasswordURLFlag = flagSet.Bool("reset-password-url", false, `Print the reset password URL to manually send to the new user.`) 31 | apiFlags = api.NewFlags(flagSet) 32 | ) 33 | 34 | handler := func(args []string) error { 35 | if err := flagSet.Parse(args); err != nil { 36 | return err 37 | } 38 | 39 | client := cfg.apiClient(apiFlags, flagSet.Output()) 40 | 41 | query := `mutation CreateUser( 42 | $username: String!, 43 | $email: String!, 44 | ) { 45 | createUser( 46 | username: $username, 47 | email: $email, 48 | ) { 49 | resetPasswordURL 50 | } 51 | }` 52 | 53 | var result struct { 54 | CreateUser struct { 55 | ResetPasswordURL string 56 | } 57 | } 58 | if ok, err := client.NewRequest(query, map[string]interface{}{ 59 | "username": *usernameFlag, 60 | "email": *emailFlag, 61 | }).Do(context.Background(), &result); err != nil || !ok { 62 | return err 63 | } 64 | 65 | fmt.Printf("User %q created.\n", *usernameFlag) 66 | if *resetPasswordURLFlag && result.CreateUser.ResetPasswordURL != "" { 67 | fmt.Println() 68 | fmt.Printf("\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) 69 | } 70 | return nil 71 | } 72 | 73 | // Register the command. 74 | usersCommands = append(usersCommands, &command{ 75 | flagSet: flagSet, 76 | handler: handler, 77 | usageFunc: usageFunc, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/src/users_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | 10 | "github.com/sourcegraph/src-cli/internal/api" 11 | ) 12 | 13 | func init() { 14 | usage := ` 15 | Examples: 16 | 17 | Get user with username alice: 18 | 19 | $ src users get -username=alice 20 | 21 | ` 22 | 23 | flagSet := flag.NewFlagSet("get", flag.ExitOnError) 24 | usageFunc := func() { 25 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) 26 | flagSet.PrintDefaults() 27 | fmt.Println(usage) 28 | } 29 | var ( 30 | usernameFlag = flagSet.String("username", "", `Look up user by username. (e.g. "alice")`) 31 | emailFlag = flagSet.String("email", "", `Look up user by email. (e.g. "alice@sourcegraph.com")`) 32 | formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`) 33 | apiFlags = api.NewFlags(flagSet) 34 | ) 35 | 36 | handler := func(args []string) error { 37 | if err := flagSet.Parse(args); err != nil { 38 | return err 39 | } 40 | 41 | client := cfg.apiClient(apiFlags, flagSet.Output()) 42 | 43 | if usernameFlag != nil && *usernameFlag != "" && emailFlag != nil && *emailFlag != "" { 44 | return errors.New("cannot specify both email and username") 45 | } 46 | 47 | tmpl, err := parseTemplate(*formatFlag) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | query := `query User( 53 | $username: String, 54 | $email: String, 55 | ) { 56 | user( 57 | username: $username, 58 | email: $email, 59 | ) { 60 | ...UserFields 61 | } 62 | }` + userFragment 63 | 64 | var result struct { 65 | User *User 66 | } 67 | if ok, err := client.NewRequest(query, map[string]interface{}{ 68 | "username": api.NullString(*usernameFlag), 69 | "email": api.NullString(*emailFlag), 70 | }).Do(context.Background(), &result); err != nil || !ok { 71 | return err 72 | } 73 | 74 | return execTemplate(tmpl, result.User) 75 | } 76 | 77 | // Register the command. 78 | usersCommands = append(usersCommands, &command{ 79 | flagSet: flagSet, 80 | handler: handler, 81 | usageFunc: usageFunc, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/src/users_tag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/sourcegraph/src-cli/internal/api" 9 | ) 10 | 11 | func init() { 12 | usage := ` 13 | Examples: 14 | 15 | Add a tag "foo" to a user: 16 | 17 | $ src users tag -user-id=$(src users get -f '{{.ID}}' -username=alice) -tag=foo 18 | 19 | Remove a tag "foo" to a user: 20 | 21 | $ src users tag -user-id=$(src users get -f '{{.ID}}' -username=alice) -remove -tag=foo 22 | 23 | Related examples: 24 | 25 | List all users with the "foo" tag: 26 | 27 | $ src users list -tag=foo 28 | 29 | ` 30 | 31 | flagSet := flag.NewFlagSet("tag", flag.ExitOnError) 32 | usageFunc := func() { 33 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) 34 | flagSet.PrintDefaults() 35 | fmt.Println(usage) 36 | } 37 | var ( 38 | userIDFlag = flagSet.String("user-id", "", `The ID of the user to tag. (required)`) 39 | tagFlag = flagSet.String("tag", "", `The tag to set on the user. (required)`) 40 | removeFlag = flagSet.Bool("remove", false, `Remove the tag. (default: add the tag`) 41 | apiFlags = api.NewFlags(flagSet) 42 | ) 43 | 44 | handler := func(args []string) error { 45 | if err := flagSet.Parse(args); err != nil { 46 | return err 47 | } 48 | 49 | client := cfg.apiClient(apiFlags, flagSet.Output()) 50 | 51 | query := `mutation SetUserTag( 52 | $user: ID!, 53 | $tag: String!, 54 | $present: Boolean! 55 | ) { 56 | setTag( 57 | node: $user, 58 | tag: $tag, 59 | present: $present 60 | ) { 61 | alwaysNil 62 | } 63 | }` 64 | 65 | _, err := client.NewRequest(query, map[string]interface{}{ 66 | "user": *userIDFlag, 67 | "tag": *tagFlag, 68 | "present": !*removeFlag, 69 | }).Do(context.Background(), &struct{}{}) 70 | return err 71 | } 72 | 73 | // Register the command. 74 | usersCommands = append(usersCommands, &command{ 75 | flagSet: flagSet, 76 | handler: handler, 77 | usageFunc: usageFunc, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/src/validate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | var validateCommands commander 9 | 10 | func init() { 11 | usage := `'src validate' is a tool that validates a Sourcegraph instance. 12 | 13 | EXPERIMENTAL: 'validate' is an experimental command in the 'src' tool. 14 | 15 | Please visit https://docs.sourcegraph.com/admin/validation for documentation of the validate command. 16 | 17 | Usage: 18 | 19 | src validate command [command options] 20 | 21 | The commands are: 22 | 23 | install validates a Sourcegraph installation 24 | kube validates a Sourcegraph deployment on a Kubernetes cluster 25 | 26 | Use "src validate [command] -h" for more information about a command. 27 | ` 28 | 29 | flagSet := flag.NewFlagSet("validate", flag.ExitOnError) 30 | handler := func(args []string) error { 31 | validateCommands.run(flagSet, "src validate", usage, args) 32 | return nil 33 | } 34 | 35 | // Register the command 36 | commands = append(commands, &command{ 37 | flagSet: flagSet, 38 | aliases: []string{"validate"}, 39 | handler: handler, 40 | usageFunc: func() { 41 | fmt.Println(usage) 42 | }, 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /contrib/.gitignore: -------------------------------------------------------------------------------- 1 | # for `nix build` 2 | result 3 | -------------------------------------------------------------------------------- /contrib/default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).defaultNix 11 | -------------------------------------------------------------------------------- /contrib/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1673956053, 7 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "locked": { 21 | "lastModified": 1678901627, 22 | "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", 23 | "owner": "numtide", 24 | "repo": "flake-utils", 25 | "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "type": "github" 32 | } 33 | }, 34 | "nixpkgs": { 35 | "locked": { 36 | "lastModified": 1680125544, 37 | "narHash": "sha256-mlqo1r+TZUOuypWdrZHluxWL+E5WzXlUXNZ9Y0WLDFU=", 38 | "owner": "NixOS", 39 | "repo": "nixpkgs", 40 | "rev": "9a6aabc4740790ef3bbb246b86d029ccf6759658", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "id": "nixpkgs", 45 | "ref": "nixos-unstable", 46 | "type": "indirect" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-compat": "flake-compat", 52 | "flake-utils": "flake-utils", 53 | "nixpkgs": "nixpkgs" 54 | } 55 | } 56 | }, 57 | "root": "root", 58 | "version": 7 59 | } 60 | -------------------------------------------------------------------------------- /contrib/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Sourcegraph CLI"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | flake-compat = { 8 | url = "github:edolstra/flake-compat"; 9 | flake = false; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, ... }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | pkgs = import nixpkgs { inherit system; }; 17 | in 18 | rec { 19 | packages.src-cli = with pkgs; buildGoModule.override { go = pkgs.go_1_19; } { 20 | pname = "src-cli"; 21 | version = self.shortRev or "dirty"; 22 | src = ../.; 23 | 24 | subPackages = [ "cmd/src" ]; 25 | 26 | vendorSha256 = "sha256-NMLrBYGscZexnR43I4Ku9aqzJr38z2QAnZo0RouHFrc="; 27 | }; 28 | packages.default = packages.src-cli; 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | .bin 2 | -------------------------------------------------------------------------------- /dev/go-lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | trap "echo ^^^ +++" ERR 4 | 5 | set -ex 6 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 7 | 8 | export GOBIN="$PWD/dev/.bin" 9 | export PATH=$GOBIN:$PATH 10 | export GO111MODULE=on 11 | 12 | if [ $# -eq 0 ]; then 13 | pkgs=('./...') 14 | else 15 | pkgs=("$@") 16 | fi 17 | 18 | "./dev/golangci-lint.sh" --config .golangci.yml run "${pkgs[@]}" 19 | -------------------------------------------------------------------------------- /dev/golangci-lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euf -o pipefail 4 | 5 | pushd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 6 | 7 | mkdir -p dev/.bin 8 | 9 | version="1.64.5" 10 | suffix="${version}-$(go env GOOS)-$(go env GOARCH)" 11 | target="$PWD/dev/.bin/golangci-lint-${suffix}" 12 | url="https://github.com/golangci/golangci-lint/releases/download/v${version}/golangci-lint-${suffix}.tar.gz" 13 | 14 | if [ ! -f "${target}" ]; then 15 | echo "downloading ${url}" 1>&2 16 | curl -sS -L -f "${url}" -o "${target}.tar.gz" 17 | tar xzf "${target}.tar.gz" 18 | mv "golangci-lint-${suffix}/golangci-lint" "${target}" 19 | rm -f "${target}.tar.gz" 20 | rm -rf "golangci-lint-${suffix}" 21 | echo "downloaded" 1>&2 22 | fi 23 | 24 | chmod +x "${target}" 25 | 26 | popd >/dev/null 27 | 28 | exec "${target}" "$@" 29 | -------------------------------------------------------------------------------- /docker/batch-change-volume-workspace/Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile builds the sourcegraph/src-batch-change-volume-workspace 2 | # image that we use to run curl, git, and unzip against a Docker volume when 3 | # using the volume workspace. 4 | 5 | FROM alpine:3.19.1@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b 6 | 7 | RUN apk add --update git unzip 8 | -------------------------------------------------------------------------------- /docker/batch-change-volume-workspace/README.md: -------------------------------------------------------------------------------- 1 | # `src` volume workspace base image 2 | 3 | Sourcegraph `src` executes batch changes using either a bind or volume workspace. In the latter case (which is the default on macOS), this utility image is used to initialise the volume workspace within Docker, and then to extract the diff used when creating the changeset. 4 | 5 | This image is based on Alpine, and adds the tools we need: curl, git, and unzip. 6 | 7 | For more information, please refer to the [`src-cli` repository](https://github.com/sourcegraph/src-cli/tree/main/docker/batch-change-volume-workspace). 8 | 9 | 14 | -------------------------------------------------------------------------------- /internal/api/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "api", 5 | srcs = [ 6 | "api.go", 7 | "errors.go", 8 | "flags.go", 9 | "gzip.go", 10 | "nullable.go", 11 | "proxy.go", 12 | "test_unix_socket_server.go", 13 | ], 14 | importpath = "github.com/sourcegraph/src-cli/internal/api", 15 | visibility = ["//:__subpackages__"], 16 | deps = [ 17 | "//internal/version", 18 | "@com_github_jig_teereadcloser//:teereadcloser", 19 | "@com_github_kballard_go_shellquote//:go-shellquote", 20 | "@com_github_mattn_go_isatty//:go-isatty", 21 | "@com_github_sourcegraph_sourcegraph_lib//errors", 22 | ], 23 | ) 24 | 25 | go_test( 26 | name = "api_test", 27 | srcs = [ 28 | "api_test.go", 29 | "errors_test.go", 30 | "gzip_test.go", 31 | ], 32 | embed = [":api"], 33 | deps = ["@com_github_google_go_cmp//cmp"], 34 | ) 35 | -------------------------------------------------------------------------------- /internal/api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // TODO: implement a super basic GraphQL server that can return canned results. 4 | -------------------------------------------------------------------------------- /internal/api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/sourcegraph/sourcegraph/lib/errors" 7 | ) 8 | 9 | // GraphQlErrors contains one or more GraphQlError instances. 10 | type GraphQlErrors []*GraphQlError 11 | 12 | func (gg GraphQlErrors) Error() string { 13 | // This slightly convoluted implementation is used to ensure that output 14 | // remains stable with earlier versions of src-cli, which returned a wrapped 15 | // *multierror.Error when GraphQL errors were returned from the API. 16 | 17 | if len(gg) == 0 { 18 | // This shouldn't really happen, but let's handle it gracefully anyway. 19 | return "" 20 | } 21 | 22 | var errs errors.MultiError 23 | for _, err := range gg { 24 | errs = errors.Append(errs, err) 25 | } 26 | 27 | return errors.Wrap(errs, "GraphQL errors").Error() 28 | } 29 | 30 | // GraphQlError wraps a raw JSON error returned from a GraphQL endpoint. 31 | type GraphQlError struct{ v interface{} } 32 | 33 | // Code returns the GraphQL error code, if one was set on the error. 34 | func (g *GraphQlError) Code() (string, error) { 35 | ext, err := g.Extensions() 36 | if err != nil { 37 | return "", errors.Wrap(err, "getting error extensions") 38 | } 39 | 40 | if ext != nil { 41 | if ext["code"] == nil { 42 | return "", nil 43 | } else if code, ok := ext["code"].(string); ok { 44 | return code, nil 45 | } 46 | return "", errors.Errorf("unexpected code of type %T", ext["code"]) 47 | } 48 | return "", nil 49 | } 50 | 51 | func (g *GraphQlError) Error() string { 52 | j, _ := json.MarshalIndent(g.v, "", " ") 53 | return string(j) 54 | } 55 | 56 | // Extensions returns the GraphQL error extensions, if set, or nil if no 57 | // extensions were set on the error. 58 | func (g *GraphQlError) Extensions() (map[string]interface{}, error) { 59 | e, ok := g.v.(map[string]interface{}) 60 | if !ok { 61 | return nil, errors.Errorf("unexpected GraphQL error of type %T", g.v) 62 | } 63 | 64 | if e["extensions"] == nil { 65 | return nil, nil 66 | } else if me, ok := e["extensions"].(map[string]interface{}); ok { 67 | return me, nil 68 | } 69 | return nil, errors.Errorf("unexpected extensions of type %T", e["extensions"]) 70 | } 71 | 72 | var ( 73 | _ error = &GraphQlError{} 74 | _ error = GraphQlErrors{} 75 | ) 76 | -------------------------------------------------------------------------------- /internal/api/flags.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | ) 7 | 8 | // Flags encapsulates the standard flags that should be added to all commands 9 | // that issue API requests. 10 | type Flags struct { 11 | dump *bool 12 | getCurl *bool 13 | trace *bool 14 | insecureSkipVerify *bool 15 | userAgentTelemetry *bool 16 | } 17 | 18 | func (f *Flags) Trace() bool { 19 | if f.trace == nil { 20 | return false 21 | } 22 | return *(f.trace) 23 | } 24 | 25 | func (f *Flags) GetCurl() bool { 26 | if f.getCurl == nil { 27 | return false 28 | } 29 | return *(f.getCurl) 30 | } 31 | 32 | func (f *Flags) UserAgentTelemetry() bool { 33 | if f.userAgentTelemetry == nil { 34 | return defaultUserAgentTelemetry() 35 | } 36 | return *(f.userAgentTelemetry) 37 | } 38 | 39 | // NewFlags instantiates a new Flags structure and attaches flags to the given 40 | // flag set. 41 | func NewFlags(flagSet *flag.FlagSet) *Flags { 42 | return &Flags{ 43 | dump: flagSet.Bool("dump-requests", false, "Log GraphQL requests and responses to stdout"), 44 | getCurl: flagSet.Bool("get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"), 45 | trace: flagSet.Bool("trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"), 46 | insecureSkipVerify: flagSet.Bool("insecure-skip-verify", false, "Skip validation of TLS certificates against trusted chains"), 47 | userAgentTelemetry: flagSet.Bool("user-agent-telemetry", defaultUserAgentTelemetry(), "Include the operating system and architecture in the User-Agent sent with requests to Sourcegraph"), 48 | } 49 | } 50 | 51 | func defaultFlags() *Flags { 52 | telemetry := defaultUserAgentTelemetry() 53 | d := false 54 | return &Flags{ 55 | dump: &d, 56 | getCurl: &d, 57 | trace: &d, 58 | insecureSkipVerify: &d, 59 | userAgentTelemetry: &telemetry, 60 | } 61 | } 62 | 63 | func defaultUserAgentTelemetry() bool { 64 | return os.Getenv("SRC_DISABLE_USER_AGENT_TELEMETRY") == "" 65 | } 66 | -------------------------------------------------------------------------------- /internal/api/gzip.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | 7 | "github.com/sourcegraph/sourcegraph/lib/errors" 8 | ) 9 | 10 | // gzipReader decorates a source reader by gzip compressing its contents. 11 | func gzipReader(source io.Reader) io.Reader { 12 | r, w := io.Pipe() 13 | go func() { 14 | // propagate gzip write errors into new reader 15 | w.CloseWithError(gzipPipe(source, w)) 16 | }() 17 | return r 18 | } 19 | 20 | // gzipPipe reads uncompressed data from r and writes compressed data to w. 21 | func gzipPipe(r io.Reader, w io.Writer) (err error) { 22 | gzipWriter := gzip.NewWriter(w) 23 | defer func() { 24 | if closeErr := gzipWriter.Close(); closeErr != nil { 25 | err = errors.Append(err, err) 26 | } 27 | }() 28 | 29 | _, err = io.Copy(gzipWriter, r) 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /internal/api/gzip_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | func TestGzipReader(t *testing.T) { 13 | var uncompressed []byte 14 | for i := 0; i < 20000; i++ { 15 | uncompressed = append(uncompressed, byte(i)) 16 | } 17 | 18 | contents, err := io.ReadAll(gzipReader(bytes.NewReader(uncompressed))) 19 | if err != nil { 20 | t.Fatalf("unexpected error reading from gzip reader: %s", err) 21 | } 22 | 23 | gzipReader, err := gzip.NewReader(bytes.NewReader(contents)) 24 | if err != nil { 25 | t.Fatalf("unexpected error creating gzip.Reader: %s", err) 26 | } 27 | decompressed, err := io.ReadAll(gzipReader) 28 | if err != nil { 29 | t.Fatalf("unexpected error reading from gzip.Reader: %s", err) 30 | } 31 | if diff := cmp.Diff(decompressed, uncompressed); diff != "" { 32 | t.Errorf("unexpected gzipped contents (-want +got):\n%s", diff) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/api/mock/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "mock", 5 | srcs = ["api.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/api/mock", 7 | visibility = ["//:__subpackages__"], 8 | deps = [ 9 | "//internal/api", 10 | "@com_github_stretchr_testify//mock", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /internal/api/mock/api.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/sourcegraph/src-cli/internal/api" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | type Client struct { 14 | mock.Mock 15 | } 16 | 17 | func (m *Client) NewQuery(query string) api.Request { 18 | args := m.Called(query) 19 | return args.Get(0).(api.Request) 20 | } 21 | 22 | func (m *Client) NewRequest(query string, vars map[string]interface{}) api.Request { 23 | args := m.Called(query, vars) 24 | return args.Get(0).(api.Request) 25 | } 26 | 27 | func (m *Client) NewGzippedRequest(query string, vars map[string]interface{}) api.Request { 28 | args := m.Called(query, vars) 29 | return args.Get(0).(api.Request) 30 | } 31 | 32 | func (m *Client) NewGzippedQuery(query string) api.Request { 33 | args := m.Called(query) 34 | return args.Get(0).(api.Request) 35 | } 36 | 37 | func (m *Client) NewHTTPRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { 38 | args := m.Called(ctx, method, path, body) 39 | var obj *http.Request 40 | if args.Get(0) != nil { 41 | obj = args.Get(0).(*http.Request) 42 | } 43 | return obj, args.Error(1) 44 | } 45 | 46 | func (m *Client) Do(req *http.Request) (*http.Response, error) { 47 | args := m.Called(req) 48 | var obj *http.Response 49 | if args.Get(0) != nil { 50 | obj = args.Get(0).(*http.Response) 51 | } 52 | return obj, args.Error(1) 53 | } 54 | 55 | type Request struct { 56 | mock.Mock 57 | Response string 58 | } 59 | 60 | func (r *Request) Do(ctx context.Context, result interface{}) (bool, error) { 61 | args := r.Called(ctx, result) 62 | if r.Response != "" { 63 | if err := json.Unmarshal([]byte(r.Response), result); err != nil { 64 | return false, err 65 | } 66 | } 67 | return args.Bool(0), args.Error(1) 68 | } 69 | 70 | func (r *Request) DoRaw(ctx context.Context, result interface{}) (bool, error) { 71 | args := r.Called(ctx, result) 72 | if r.Response != "" { 73 | if err := json.Unmarshal([]byte(r.Response), result); err != nil { 74 | return false, err 75 | } 76 | } 77 | return args.Bool(0), args.Error(1) 78 | } 79 | -------------------------------------------------------------------------------- /internal/api/nullable.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // NullInt returns a nullable int for use in a GraphQL variable, where -1 is 4 | // treated as a nil value. 5 | func NullInt(n int) *int { 6 | if n == -1 { 7 | return nil 8 | } 9 | return &n 10 | } 11 | 12 | // NullString returns a nullable string for use in a GraphQL variable, where "" 13 | // is treated as a nil value. 14 | func NullString(s string) *string { 15 | if s == "" { 16 | return nil 17 | } 18 | return &s 19 | } 20 | -------------------------------------------------------------------------------- /internal/batches/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "batches", 5 | srcs = [ 6 | "errors.go", 7 | "features.go", 8 | "license.go", 9 | ], 10 | importpath = "github.com/sourcegraph/src-cli/internal/batches", 11 | visibility = ["//:__subpackages__"], 12 | deps = [ 13 | "//internal/batches/graphql", 14 | "@com_github_sourcegraph_sourcegraph_lib//api", 15 | "@com_github_sourcegraph_sourcegraph_lib//errors", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /internal/batches/debug.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | // +build debug 3 | 4 | package batches 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | // In builds with the debug flag (i.e. `go build -tags debug -o src ./cmd/src`) 13 | // init() sets up the default logger to log to a file in ~/.sourcegraph. 14 | func init() { 15 | homedir, err := os.UserHomeDir() 16 | if err != nil { 17 | log.Fatalf("getting user home directory: %s", err) 18 | } 19 | 20 | fullPath := filepath.Join(homedir, ".sourcegraph", "src-cli.debug.log") 21 | 22 | f, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 23 | if err != nil { 24 | log.Fatalf("setting debug log file failed: %s", err) 25 | } 26 | 27 | log.SetOutput(f) 28 | } 29 | -------------------------------------------------------------------------------- /internal/batches/docker/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "docker", 5 | srcs = [ 6 | "cache.go", 7 | "context.go", 8 | "image.go", 9 | "info.go", 10 | "version.go", 11 | ], 12 | importpath = "github.com/sourcegraph/src-cli/internal/batches/docker", 13 | visibility = ["//:__subpackages__"], 14 | deps = [ 15 | "//internal/exec", 16 | "@com_github_kballard_go_shellquote//:go-shellquote", 17 | "@com_github_sourcegraph_sourcegraph_lib//errors", 18 | ], 19 | ) 20 | 21 | go_test( 22 | name = "docker_test", 23 | srcs = [ 24 | "cache_test.go", 25 | "image_test.go", 26 | "info_test.go", 27 | "main_test.go", 28 | ], 29 | embed = [":docker"], 30 | deps = [ 31 | "//internal/exec/expect", 32 | "@com_github_google_go_cmp//cmp", 33 | "@com_github_stretchr_testify//assert", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /internal/batches/docker/cache.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/sourcegraph/sourcegraph/lib/errors" 8 | ) 9 | 10 | type ImageCache interface { 11 | Get(name string) Image 12 | Ensure(ctx context.Context, name string) (Image, error) 13 | } 14 | 15 | // imageCache is a cache of metadata about Docker images, indexed by name. 16 | type imageCache struct { 17 | images map[string]Image 18 | imagesMu sync.Mutex 19 | } 20 | 21 | // NewImageCache creates a new image cache. 22 | func NewImageCache() ImageCache { 23 | return &imageCache{ 24 | images: make(map[string]Image), 25 | } 26 | } 27 | 28 | // Get returns the image cache entry for the given Docker image. The name may be 29 | // anything the Docker command line will accept as an image name: this will 30 | // generally be IMAGE or IMAGE:TAG. 31 | func (ic *imageCache) Get(name string) Image { 32 | ic.imagesMu.Lock() 33 | defer ic.imagesMu.Unlock() 34 | 35 | if image, ok := ic.images[name]; ok { 36 | return image 37 | } 38 | 39 | image := &image{name: name} 40 | ic.images[name] = image 41 | return image 42 | } 43 | 44 | // Ensure returns the image cache entry for the given Docker image and makes sure 45 | // it exists on disk. 46 | func (ic *imageCache) Ensure(ctx context.Context, name string) (Image, error) { 47 | img := ic.Get(name) 48 | 49 | if err := img.Ensure(ctx); err != nil { 50 | return nil, errors.Wrapf(err, "pulling image %q", name) 51 | } 52 | 53 | return img, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/batches/docker/cache_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "testing" 4 | 5 | func TestImageCache(t *testing.T) { 6 | cache := NewImageCache() 7 | if cache == nil { 8 | t.Error("unexpected nil cache") 9 | } 10 | 11 | have := cache.Get("foo") 12 | if have == nil { 13 | t.Error("unexpected nil error") 14 | } 15 | if name := have.(*image).name; name != "foo" { 16 | t.Errorf("invalid name: have=%q want=%q", name, "foo") 17 | } 18 | 19 | again := cache.Get("foo") 20 | if have != again { 21 | t.Errorf("invalid memoisation: first=%v second=%v", have, again) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/batches/docker/info.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "github.com/sourcegraph/src-cli/internal/exec" 9 | 10 | "github.com/sourcegraph/sourcegraph/lib/errors" 11 | ) 12 | 13 | // CurrentContext returns the name of the current Docker context (not to be 14 | // confused with a Go context). 15 | func CurrentContext(ctx context.Context) (string, error) { 16 | dctx, cancel, err := withFastCommandContext(ctx) 17 | if err != nil { 18 | return "", err 19 | } 20 | defer cancel() 21 | 22 | args := []string{"context", "inspect", "--format", "{{ .Name }}"} 23 | out, err := exec.CommandContext(dctx, "docker", args...).CombinedOutput() 24 | if errors.IsDeadlineExceeded(err) || errors.IsDeadlineExceeded(dctx.Err()) { 25 | return "", newFastCommandTimeoutError(dctx, args...) 26 | } else if err != nil { 27 | return "", err 28 | } 29 | 30 | name := string(bytes.TrimSpace(out)) 31 | if name == "" { 32 | return "", errors.New("no context returned from Docker") 33 | } 34 | 35 | return name, nil 36 | } 37 | 38 | type Info struct { 39 | Host struct { 40 | CPUs int `json:"cpus"` 41 | } `json:"host"` // Podman engine 42 | NCPU int `json:"NCPU"` // Docker Engine 43 | } 44 | 45 | // NCPU returns the number of CPU cores available to Docker. 46 | func NCPU(ctx context.Context) (int, error) { 47 | dctx, cancel, err := withFastCommandContext(ctx) 48 | if err != nil { 49 | return 0, err 50 | } 51 | defer cancel() 52 | 53 | args := []string{"info", "--format", "{{ json .}}"} 54 | out, err := exec.CommandContext(dctx, "docker", args...).CombinedOutput() 55 | if errors.IsDeadlineExceeded(err) || errors.IsDeadlineExceeded(dctx.Err()) { 56 | return 0, newFastCommandTimeoutError(dctx, args...) 57 | } else if err != nil { 58 | return 0, err 59 | } 60 | 61 | var info Info 62 | if err := json.Unmarshal(out, &info); err != nil { 63 | return 0, err 64 | } 65 | if info.NCPU > 0 { 66 | return info.NCPU, nil 67 | } 68 | return info.Host.CPUs, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/batches/docker/main_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/sourcegraph/src-cli/internal/exec/expect" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | code := expect.Handle(m) 12 | os.Exit(code) 13 | } 14 | -------------------------------------------------------------------------------- /internal/batches/docker/version.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // CheckVersion is used to check if docker is running. We use this method instead of 9 | // checkExecutable (https://sourcegraph.com/github.com/sourcegraph/src-cli@main/-/blob/cmd/src/batch_common.go?L547%3A6=&popover=pinned) 10 | // to prevent a case where docker commands take too long and results in `src-cli` freezing for some users. 11 | func CheckVersion(ctx context.Context) error { 12 | _, err := executeFastCommand(ctx, "version") 13 | if err != nil { 14 | return fmt.Errorf( 15 | "failed to execute \"docker version\":\n\t%s\n\n'src batch' requires \"docker\" to be available.", 16 | err, 17 | ) 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/batches/errors.go: -------------------------------------------------------------------------------- 1 | package batches 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/sourcegraph/src-cli/internal/batches/graphql" 8 | ) 9 | 10 | // TODO(mrnugget): Merge these two types (give them an "errorfmt" function, 11 | // rename "Has*" methods to "NotEmpty" or something) 12 | 13 | // UnsupportedRepoSet provides a set to manage repositories that are on 14 | // unsupported code hosts. This type implements error to allow it to be 15 | // returned directly as an error value if needed. 16 | type UnsupportedRepoSet map[*graphql.Repository]struct{} 17 | 18 | func (e UnsupportedRepoSet) Includes(r *graphql.Repository) bool { 19 | _, ok := e[r] 20 | return ok 21 | } 22 | 23 | func (e UnsupportedRepoSet) Error() string { 24 | repos := []string{} 25 | typeSet := map[string]struct{}{} 26 | for repo := range e { 27 | repos = append(repos, repo.Name) 28 | typeSet[repo.ExternalRepository.ServiceType] = struct{}{} 29 | } 30 | 31 | types := []string{} 32 | for t := range typeSet { 33 | types = append(types, t) 34 | } 35 | 36 | return fmt.Sprintf( 37 | "found repositories on unsupported code hosts: %s\nrepositories:\n\t%s", 38 | strings.Join(types, ", "), 39 | strings.Join(repos, "\n\t"), 40 | ) 41 | } 42 | 43 | func (e UnsupportedRepoSet) Append(repo *graphql.Repository) { 44 | e[repo] = struct{}{} 45 | } 46 | 47 | func (e UnsupportedRepoSet) HasUnsupported() bool { 48 | return len(e) > 0 49 | } 50 | 51 | // IgnoredRepoSet provides a set to manage repositories that are on 52 | // unsupported code hosts. This type implements error to allow it to be 53 | // returned directly as an error value if needed. 54 | type IgnoredRepoSet map[*graphql.Repository]struct{} 55 | 56 | func (e IgnoredRepoSet) Includes(r *graphql.Repository) bool { 57 | _, ok := e[r] 58 | return ok 59 | } 60 | 61 | func (e IgnoredRepoSet) Error() string { 62 | repos := []string{} 63 | for repo := range e { 64 | repos = append(repos, repo.Name) 65 | } 66 | 67 | return fmt.Sprintf( 68 | "found repositories containing .batchignore files:\n\t%s", 69 | strings.Join(repos, "\n\t"), 70 | ) 71 | } 72 | 73 | func (e IgnoredRepoSet) Append(repo *graphql.Repository) { 74 | e[repo] = struct{}{} 75 | } 76 | 77 | func (e IgnoredRepoSet) HasIgnored() bool { 78 | return len(e) > 0 79 | } 80 | -------------------------------------------------------------------------------- /internal/batches/executor/main_test.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | batcheslib "github.com/sourcegraph/sourcegraph/lib/batches" 5 | "github.com/sourcegraph/sourcegraph/lib/batches/overridable" 6 | 7 | "github.com/sourcegraph/src-cli/internal/batches/graphql" 8 | ) 9 | 10 | var testRepo1 = &graphql.Repository{ 11 | ID: "src-cli", 12 | Name: "github.com/sourcegraph/src-cli", 13 | DefaultBranch: &graphql.Branch{Name: "main", Target: graphql.Target{OID: "d34db33f"}}, 14 | FileMatches: map[string]bool{ 15 | "README.md": true, 16 | "main.go": true, 17 | }, 18 | } 19 | 20 | var testRepo2 = &graphql.Repository{ 21 | ID: "sourcegraph", 22 | Name: "github.com/sourcegraph/sourcegraph", 23 | DefaultBranch: &graphql.Branch{ 24 | Name: "main", 25 | Target: graphql.Target{OID: "f00b4r3r"}, 26 | }, 27 | } 28 | 29 | var testPublished = overridable.FromBoolOrString(false) 30 | 31 | var testChangesetTemplate = &batcheslib.ChangesetTemplate{ 32 | Title: "commit title", 33 | Body: "commit body", 34 | Branch: "commit-branch", 35 | Commit: batcheslib.ExpandedGitCommitDescription{ 36 | Message: "commit msg", 37 | Author: &batcheslib.GitCommitAuthor{ 38 | Name: "Tester", 39 | Email: "tester@example.com", 40 | }, 41 | }, 42 | Published: &testPublished, 43 | } 44 | -------------------------------------------------------------------------------- /internal/batches/features.go: -------------------------------------------------------------------------------- 1 | package batches 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/sourcegraph/sourcegraph/lib/api" 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | ) 10 | 11 | // FeatureFlags represent features that are only available on certain 12 | // Sourcegraph versions and we therefore have to detect at runtime. 13 | type FeatureFlags struct { 14 | Sourcegraph40 bool 15 | BinaryDiffs bool 16 | } 17 | 18 | func (ff *FeatureFlags) SetFromVersion(version string, skipErrors bool) error { 19 | for _, feature := range []struct { 20 | flag *bool 21 | constraint string 22 | minDate string 23 | }{ 24 | // NOTE: It's necessary to include a "-0" prerelease suffix on each constraint so that 25 | // prereleases of future versions are still considered to satisfy the constraint. 26 | // 27 | // For example, the version "3.35.1-rc.3" is not considered to satisfy the constraint 28 | // ">= 3.23.0". However, the same version IS considered to satisfy the constraint 29 | // "3.23.0-0". See 30 | // https://github.com/Masterminds/semver#working-with-prerelease-versions for more. 31 | // Example usage: 32 | // {&ff.FlagName, ">= 3.23.0-0", "2020-11-24"}, 33 | {&ff.Sourcegraph40, ">= 4.0.0-0", "2022-08-24"}, 34 | {&ff.BinaryDiffs, ">= 4.3.0-0", "2022-11-29"}, 35 | } { 36 | value, err := api.CheckSourcegraphVersion(version, feature.constraint, feature.minDate) 37 | if err != nil { 38 | if skipErrors { 39 | log.Printf("failed to check version returned by Sourcegraph: %s. Assuming no feature flags.", version) 40 | } else { 41 | return errors.Wrap(err, fmt.Sprintf("failed to check version returned by Sourcegraph: %s", version)) 42 | } 43 | } 44 | *feature.flag = value 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/batches/graphql/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "graphql", 5 | srcs = [ 6 | "batches.go", 7 | "repository.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/batches/graphql", 10 | visibility = ["//:__subpackages__"], 11 | deps = ["//internal/batches/util"], 12 | ) 13 | -------------------------------------------------------------------------------- /internal/batches/graphql/batches.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | type BatchSpecID string 4 | type ChangesetSpecID string 5 | 6 | type CreateBatchSpecResponse struct { 7 | ID BatchSpecID 8 | ApplyURL string 9 | } 10 | 11 | type BatchChange struct { 12 | URL string 13 | } 14 | -------------------------------------------------------------------------------- /internal/batches/graphql/repository.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/sourcegraph/src-cli/internal/batches/util" 7 | ) 8 | 9 | const RepositoryFieldsFragment = ` 10 | fragment repositoryFields on Repository { 11 | id 12 | name 13 | url 14 | externalRepository { 15 | serviceType 16 | } 17 | defaultBranch { 18 | name 19 | target { 20 | oid 21 | } 22 | } 23 | commit(rev: $rev) @include(if:$queryCommit) { 24 | oid 25 | } 26 | } 27 | ` 28 | 29 | type Target struct { 30 | OID string 31 | } 32 | 33 | type Branch struct { 34 | Name string 35 | Target Target 36 | } 37 | 38 | type Repository struct { 39 | ID string 40 | Name string 41 | URL string 42 | ExternalRepository struct{ ServiceType string } 43 | 44 | DefaultBranch *Branch 45 | 46 | Commit Target 47 | // Branch is populated by resolveRepositoryNameAndBranch with the queried 48 | // branch's name and the contents of the Commit property. 49 | Branch Branch 50 | 51 | FileMatches map[string]bool 52 | } 53 | 54 | func (r *Repository) HasBranch() bool { 55 | return r.DefaultBranch != nil || (r.Commit.OID != "" && r.Branch.Name != "") 56 | } 57 | 58 | func (r *Repository) BaseRef() string { 59 | if r.Branch.Name != "" { 60 | return util.EnsureRefPrefix(r.Branch.Name) 61 | } 62 | 63 | return util.EnsureRefPrefix(r.DefaultBranch.Name) 64 | } 65 | 66 | func (r *Repository) Rev() string { 67 | if r.Branch.Target.OID != "" { 68 | return r.Branch.Target.OID 69 | } 70 | 71 | return r.DefaultBranch.Target.OID 72 | } 73 | 74 | func (r *Repository) SortedFileMatches() []string { 75 | matches := make([]string, 0, len(r.FileMatches)) 76 | for path := range r.FileMatches { 77 | matches = append(matches, path) 78 | } 79 | sort.Strings(matches) 80 | return matches 81 | } 82 | -------------------------------------------------------------------------------- /internal/batches/license.go: -------------------------------------------------------------------------------- 1 | package batches 2 | 3 | type LicenseRestrictions struct { 4 | MaxUnlicensedChangesets int 5 | } 6 | -------------------------------------------------------------------------------- /internal/batches/log/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "log", 5 | srcs = [ 6 | "disk_manager.go", 7 | "disk_task_logger.go", 8 | "logger.go", 9 | "noop_task_logger.go", 10 | ], 11 | importpath = "github.com/sourcegraph/src-cli/internal/batches/log", 12 | visibility = ["//:__subpackages__"], 13 | deps = ["@com_github_sourcegraph_sourcegraph_lib//errors"], 14 | ) 15 | -------------------------------------------------------------------------------- /internal/batches/log/disk_manager.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/sourcegraph/sourcegraph/lib/errors" 7 | ) 8 | 9 | var _ LogManager = &DiskManager{} 10 | 11 | type DiskManager struct { 12 | dir string 13 | keepLogs bool 14 | 15 | tasks sync.Map 16 | } 17 | 18 | func NewDiskManager(dir string, keepLogs bool) *DiskManager { 19 | return &DiskManager{dir: dir, keepLogs: keepLogs} 20 | } 21 | 22 | func (lm *DiskManager) AddTask(slug string) (TaskLogger, error) { 23 | tl, err := newTaskLogger(slug, lm.keepLogs, lm.dir) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | lm.tasks.Store(slug, tl) 29 | return tl, nil 30 | } 31 | 32 | func (lm *DiskManager) Close() error { 33 | var errs errors.MultiError 34 | 35 | lm.tasks.Range(func(_, v interface{}) bool { 36 | logger := v.(*FileTaskLogger) 37 | 38 | if err := logger.Close(); err != nil { 39 | errs = errors.Append(errs, err) 40 | } 41 | 42 | return true 43 | }) 44 | 45 | return errs 46 | } 47 | 48 | func (lm *DiskManager) LogFiles() []string { 49 | var files []string 50 | 51 | lm.tasks.Range(func(_, v interface{}) bool { 52 | files = append(files, v.(*FileTaskLogger).Path()) 53 | return true 54 | }) 55 | 56 | return files 57 | } 58 | -------------------------------------------------------------------------------- /internal/batches/log/disk_task_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/sourcegraph/sourcegraph/lib/errors" 11 | ) 12 | 13 | type FileTaskLogger struct { 14 | f *os.File 15 | 16 | errored bool 17 | keep bool 18 | } 19 | 20 | func newTaskLogger(slug string, keep bool, dir string) (*FileTaskLogger, error) { 21 | prefix := "changeset-" + slug 22 | 23 | f, err := os.CreateTemp(dir, prefix+".*.log") 24 | if err != nil { 25 | return nil, errors.Wrapf(err, "creating temporary file with prefix %q", prefix) 26 | } 27 | 28 | return &FileTaskLogger{ 29 | f: f, 30 | keep: keep, 31 | }, nil 32 | } 33 | 34 | func (tl *FileTaskLogger) Close() error { 35 | if err := tl.f.Close(); err != nil { 36 | return err 37 | } 38 | 39 | if tl.errored || tl.keep { 40 | return nil 41 | } 42 | 43 | if err := os.Remove(tl.f.Name()); err != nil { 44 | return errors.Wrapf(err, "cleaning up log file %s", tl.f.Name()) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (tl *FileTaskLogger) Log(s string) { 51 | fmt.Fprintf(tl.f, "%s %s\n", time.Now().Format(time.RFC3339Nano), s) 52 | } 53 | 54 | func (tl *FileTaskLogger) Logf(format string, a ...interface{}) { 55 | fmt.Fprintf(tl.f, "%s "+format+"\n", append([]interface{}{time.Now().Format(time.RFC3339Nano)}, a...)...) 56 | } 57 | 58 | func (tl *FileTaskLogger) MarkErrored() { 59 | tl.errored = true 60 | } 61 | 62 | func (tl *FileTaskLogger) Path() string { 63 | return tl.f.Name() 64 | } 65 | 66 | func (tl *FileTaskLogger) PrefixWriter(prefix string) io.Writer { 67 | return &prefixWriter{tl, prefix} 68 | } 69 | 70 | type prefixWriter struct { 71 | logger *FileTaskLogger 72 | prefix string 73 | } 74 | 75 | func (pw *prefixWriter) Write(p []byte) (int, error) { 76 | // Don't split on the final newline in this writer, split 77 | // content into separate lines anyways, so lines without \n 78 | // at the end wouldn't print properly regardless. This fixes 79 | // output being separated by constant newlines. 80 | // Otherwise: 81 | // > echo Hello world; echo Hello Sourcegraph 82 | // 83 | // Hello world 84 | // 85 | // Hello Sourcegraph 86 | // 87 | t := bytes.TrimSuffix(p, []byte("\n")) 88 | for _, line := range bytes.Split(t, []byte("\n")) { 89 | pw.logger.Logf("%s | %s", pw.prefix, string(line)) 90 | } 91 | return len(p), nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/batches/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "io" 4 | 5 | type LogManager interface { 6 | AddTask(string) (TaskLogger, error) 7 | Close() error 8 | LogFiles() []string 9 | } 10 | 11 | type TaskLogger interface { 12 | Close() error 13 | Log(string) 14 | Logf(string, ...interface{}) 15 | MarkErrored() 16 | Path() string 17 | PrefixWriter(prefix string) io.Writer 18 | } 19 | -------------------------------------------------------------------------------- /internal/batches/log/noop_task_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type NoopTaskLogger struct{} 8 | 9 | func (tl *NoopTaskLogger) Close() error { 10 | return nil 11 | } 12 | 13 | func (tl *NoopTaskLogger) Log(s string) {} 14 | 15 | func (tl *NoopTaskLogger) Logf(format string, a ...interface{}) {} 16 | 17 | func (tl *NoopTaskLogger) MarkErrored() {} 18 | 19 | func (tl *NoopTaskLogger) Path() string { 20 | return "not-retained" 21 | } 22 | 23 | func (tl *NoopTaskLogger) PrefixWriter(prefix string) io.Writer { 24 | return io.Discard 25 | } 26 | -------------------------------------------------------------------------------- /internal/batches/mock/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "mock", 5 | srcs = [ 6 | "cache.go", 7 | "docker_image_progress.go", 8 | "image.go", 9 | "logger.go", 10 | "repo_archive.go", 11 | ], 12 | importpath = "github.com/sourcegraph/src-cli/internal/batches/mock", 13 | visibility = ["//:__subpackages__"], 14 | deps = [ 15 | "//internal/batches/docker", 16 | "//internal/batches/log", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /internal/batches/mock/cache.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sourcegraph/src-cli/internal/batches/docker" 7 | ) 8 | 9 | type ImageCache struct { 10 | Images map[string]docker.Image 11 | } 12 | 13 | var _ docker.ImageCache = &ImageCache{} 14 | 15 | func (c *ImageCache) Get(name string) docker.Image { return c.Images[name] } 16 | func (c *ImageCache) Ensure(ctx context.Context, name string) (docker.Image, error) { 17 | img := c.Images[name] 18 | return img, img.Ensure(ctx) 19 | } 20 | -------------------------------------------------------------------------------- /internal/batches/mock/docker_image_progress.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | type ProgressCall struct { 4 | Done int 5 | Total int 6 | } 7 | 8 | type Progress struct { 9 | Calls []ProgressCall 10 | } 11 | 12 | func (p *Progress) Callback() func(int, int) { 13 | return func(done, total int) { 14 | p.Calls = append(p.Calls, ProgressCall{done, total}) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/batches/mock/image.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/sourcegraph/src-cli/internal/batches/docker" 7 | ) 8 | 9 | type Image struct { 10 | RawDigest string 11 | DigestErr error 12 | EnsureErr error 13 | UidGid docker.UIDGID 14 | UidGidErr error 15 | } 16 | 17 | var _ docker.Image = &Image{} 18 | 19 | func (image *Image) Digest(ctx context.Context) (string, error) { 20 | return image.RawDigest, image.DigestErr 21 | } 22 | 23 | func (image *Image) Ensure(ctx context.Context) error { 24 | return image.EnsureErr 25 | } 26 | 27 | func (image *Image) UIDGID(ctx context.Context) (docker.UIDGID, error) { 28 | return image.UidGid, image.UidGidErr 29 | } 30 | -------------------------------------------------------------------------------- /internal/batches/mock/logger.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/sourcegraph/src-cli/internal/batches/log" 8 | ) 9 | 10 | var _ log.TaskLogger = TaskNoOpLogger{} 11 | 12 | type TaskNoOpLogger struct{} 13 | 14 | func (tl TaskNoOpLogger) Close() error { return nil } 15 | func (tl TaskNoOpLogger) Log(string) {} 16 | func (tl TaskNoOpLogger) Logf(string, ...interface{}) {} 17 | func (tl TaskNoOpLogger) MarkErrored() {} 18 | func (tl TaskNoOpLogger) Path() string { return "" } 19 | func (tl TaskNoOpLogger) PrefixWriter(prefix string) io.Writer { return &bytes.Buffer{} } 20 | 21 | var _ log.LogManager = LogNoOpManager{} 22 | 23 | type LogNoOpManager struct{} 24 | 25 | func (lm LogNoOpManager) AddTask(string) (log.TaskLogger, error) { 26 | return TaskNoOpLogger{}, nil 27 | } 28 | 29 | func (lm LogNoOpManager) Close() error { 30 | return nil 31 | } 32 | func (lm LogNoOpManager) LogFiles() []string { 33 | return []string{"noop"} 34 | } 35 | -------------------------------------------------------------------------------- /internal/batches/repozip/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "repozip", 5 | srcs = [ 6 | "fetcher.go", 7 | "noop.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/batches/repozip", 10 | visibility = ["//:__subpackages__"], 11 | deps = [ 12 | "//internal/batches/util", 13 | "@com_github_sourcegraph_sourcegraph_lib//errors", 14 | ], 15 | ) 16 | 17 | go_test( 18 | name = "repozip_test", 19 | srcs = ["fetcher_test.go"], 20 | embed = [":repozip"], 21 | deps = [ 22 | "//internal/api", 23 | "//internal/batches/mock", 24 | "//internal/batches/util", 25 | "@com_github_google_go_cmp//cmp", 26 | "@com_github_google_go_cmp//cmp/cmpopts", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /internal/batches/repozip/noop.go: -------------------------------------------------------------------------------- 1 | package repozip 2 | 3 | import "context" 4 | 5 | type NoopArchive struct{} 6 | 7 | func (a *NoopArchive) Ensure(context.Context) error { 8 | return nil 9 | } 10 | func (a *NoopArchive) Close() error { 11 | return nil 12 | } 13 | func (a *NoopArchive) Path() string { 14 | return "" 15 | } 16 | func (a *NoopArchive) AdditionalFilePaths() map[string]string { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /internal/batches/service/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "service", 5 | srcs = [ 6 | "build_tasks.go", 7 | "remote.go", 8 | "service.go", 9 | ], 10 | importpath = "github.com/sourcegraph/src-cli/internal/batches/service", 11 | visibility = ["//:__subpackages__"], 12 | deps = [ 13 | "//internal/api", 14 | "//internal/batches", 15 | "//internal/batches/docker", 16 | "//internal/batches/executor", 17 | "//internal/batches/graphql", 18 | "@com_github_sourcegraph_sourcegraph_lib//batches", 19 | "@com_github_sourcegraph_sourcegraph_lib//batches/template", 20 | "@com_github_sourcegraph_sourcegraph_lib//errors", 21 | ], 22 | ) 23 | 24 | go_test( 25 | name = "service_test", 26 | srcs = [ 27 | "remote_test.go", 28 | "remote_windows_test.go", 29 | "service_test.go", 30 | ], 31 | embed = [":service"], 32 | deps = [ 33 | "//internal/api/mock", 34 | "//internal/batches/docker", 35 | "//internal/batches/graphql", 36 | "//internal/batches/mock", 37 | "@com_github_sourcegraph_sourcegraph_lib//batches", 38 | "@com_github_sourcegraph_sourcegraph_lib//errors", 39 | "@com_github_stretchr_testify//assert", 40 | "@com_github_stretchr_testify//mock", 41 | "@com_github_stretchr_testify//require", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /internal/batches/service/build_tasks.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | batcheslib "github.com/sourcegraph/sourcegraph/lib/batches" 5 | "github.com/sourcegraph/sourcegraph/lib/batches/template" 6 | 7 | "github.com/sourcegraph/src-cli/internal/batches/executor" 8 | "github.com/sourcegraph/src-cli/internal/batches/graphql" 9 | ) 10 | 11 | type RepoWorkspace struct { 12 | Repo *graphql.Repository 13 | Path string 14 | OnlyFetchWorkspace bool 15 | } 16 | 17 | // buildTasks returns *executor.Tasks for all the workspaces determined for the given spec. 18 | func buildTasks(attributes *template.BatchChangeAttributes, steps []batcheslib.Step, workspaces []RepoWorkspace) []*executor.Task { 19 | tasks := make([]*executor.Task, 0, len(workspaces)) 20 | 21 | for _, ws := range workspaces { 22 | task := &executor.Task{ 23 | Repository: ws.Repo, 24 | Path: ws.Path, 25 | Steps: steps, 26 | OnlyFetchWorkspace: ws.OnlyFetchWorkspace, 27 | 28 | BatchChangeAttributes: attributes, 29 | } 30 | tasks = append(tasks, task) 31 | } 32 | 33 | return tasks 34 | } 35 | -------------------------------------------------------------------------------- /internal/batches/ui/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "ui", 5 | srcs = [ 6 | "exec_ui.go", 7 | "interval_writer.go", 8 | "json_lines.go", 9 | "task_exec_tui.go", 10 | "tty.go", 11 | "tui.go", 12 | ], 13 | importpath = "github.com/sourcegraph/src-cli/internal/batches/ui", 14 | visibility = ["//:__subpackages__"], 15 | deps = [ 16 | "//internal/api", 17 | "//internal/batches", 18 | "//internal/batches/executor", 19 | "//internal/batches/graphql", 20 | "//internal/batches/workspace", 21 | "//internal/cmderrors", 22 | "@com_github_creack_goselect//:goselect", 23 | "@com_github_derision_test_glock//:glock", 24 | "@com_github_dineshappavoo_basex//:basex", 25 | "@com_github_neelance_parallel//:parallel", 26 | "@com_github_sourcegraph_go_diff//diff", 27 | "@com_github_sourcegraph_sourcegraph_lib//batches", 28 | "@com_github_sourcegraph_sourcegraph_lib//batches/execution", 29 | "@com_github_sourcegraph_sourcegraph_lib//batches/git", 30 | "@com_github_sourcegraph_sourcegraph_lib//errors", 31 | "@com_github_sourcegraph_sourcegraph_lib//output", 32 | ], 33 | ) 34 | 35 | go_test( 36 | name = "ui_test", 37 | srcs = [ 38 | "interval_writer_test.go", 39 | "task_exec_tui_test.go", 40 | ], 41 | embed = [":ui"], 42 | deps = [ 43 | "//internal/batches/executor", 44 | "//internal/batches/graphql", 45 | "@com_github_derision_test_glock//:glock", 46 | "@com_github_google_go_cmp//cmp", 47 | "@com_github_sourcegraph_sourcegraph_lib//batches", 48 | "@com_github_sourcegraph_sourcegraph_lib//output", 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /internal/batches/ui/exec_ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/sourcegraph/src-cli/internal/batches" 5 | "github.com/sourcegraph/src-cli/internal/batches/executor" 6 | "github.com/sourcegraph/src-cli/internal/batches/graphql" 7 | "github.com/sourcegraph/src-cli/internal/batches/workspace" 8 | ) 9 | 10 | type ExecUI interface { 11 | ParsingBatchSpec() 12 | ParsingBatchSpecSuccess() 13 | ParsingBatchSpecFailure(error) 14 | 15 | ResolvingNamespace() 16 | ResolvingNamespaceSuccess(namespace string) 17 | 18 | PreparingContainerImages() 19 | PreparingContainerImagesProgress(done, total int) 20 | PreparingContainerImagesSuccess() 21 | 22 | DeterminingWorkspaceCreatorType() 23 | DeterminingWorkspaceCreatorTypeSuccess(wt workspace.CreatorType) 24 | 25 | DeterminingWorkspaces() 26 | DeterminingWorkspacesSuccess(workspacesCount, reposCount int, unsupported batches.UnsupportedRepoSet, ignored batches.IgnoredRepoSet) 27 | 28 | CheckingCache() 29 | CheckingCacheSuccess(cachedSpecsFound int, tasksToExecute int) 30 | 31 | ExecutingTasks(verbose bool, parallelism int) executor.TaskExecutionUI 32 | ExecutingTasksSkippingErrors(err error) 33 | 34 | LogFilesKept(files []string) 35 | 36 | NoChangesetSpecs() 37 | UploadingChangesetSpecs(num int) 38 | UploadingChangesetSpecsProgress(done, total int) 39 | UploadingChangesetSpecsSuccess(ids []graphql.ChangesetSpecID) 40 | 41 | CreatingBatchSpec() 42 | CreatingBatchSpecSuccess(previewURL string) 43 | CreatingBatchSpecError(maxUnlicensedCS int, err error) error 44 | 45 | PreviewBatchSpec(previewURL string) 46 | 47 | ApplyingBatchSpec() 48 | ApplyingBatchSpecSuccess(batchChangeURL string) 49 | 50 | ExecutionError(error) 51 | 52 | UploadingWorkspaceFiles() 53 | UploadingWorkspaceFilesWarning(error) 54 | UploadingWorkspaceFilesSuccess() 55 | 56 | DockerWatchDogWarning(error) 57 | } 58 | -------------------------------------------------------------------------------- /internal/batches/ui/interval_writer_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/derision-test/glock" 9 | ) 10 | 11 | func TestIntervalWriter(t *testing.T) { 12 | ctx, cancel := context.WithCancel(context.Background()) 13 | 14 | ch := make(chan string, 500) 15 | 16 | sink := func(data string) { 17 | ch <- data 18 | } 19 | 20 | ticker := glock.NewMockTicker(1 * time.Second) 21 | writer := newIntervalProcessWriter(ctx, ticker, sink) 22 | 23 | stdoutWriter := writer.StdoutWriter() 24 | stderrWriter := writer.StderrWriter() 25 | stdoutWriter.Write([]byte("1")) 26 | stderrWriter.Write([]byte("1")) 27 | select { 28 | case <-ch: 29 | t.Fatalf("ch has data") 30 | default: 31 | } 32 | 33 | ticker.BlockingAdvance(1 * time.Second) 34 | 35 | select { 36 | case d := <-ch: 37 | want := "stdout: 1\nstderr: 1\n" 38 | if d != want { 39 | t.Fatalf("wrong data in sink. want=%q, have=%q", want, d) 40 | } 41 | case <-time.After(1 * time.Second): 42 | t.Fatalf("ch has NO data") 43 | } 44 | 45 | stdoutWriter.Write([]byte("2")) 46 | stderrWriter.Write([]byte("2")) 47 | stdoutWriter.Write([]byte("3")) 48 | stderrWriter.Write([]byte("3")) 49 | stdoutWriter.Write([]byte("4")) 50 | stderrWriter.Write([]byte("4")) 51 | stdoutWriter.Write([]byte("5")) 52 | stderrWriter.Write([]byte("5")) 53 | stdoutWriter.Write([]byte(`Hello world: 1 54 | `)) 55 | stderrWriter.Write([]byte(`Hello world: 1 56 | `)) 57 | 58 | select { 59 | case <-ch: 60 | t.Fatalf("ch has data") 61 | default: 62 | } 63 | 64 | cancel() 65 | writer.Close() 66 | 67 | select { 68 | case d := <-ch: 69 | want := "stdout: 2\nstderr: 2\n" + 70 | "stdout: 3\nstderr: 3\n" + 71 | "stdout: 4\nstderr: 4\n" + 72 | "stdout: 5\nstderr: 5\n" + 73 | "stdout: Hello world: 1\nstderr: Hello world: 1\n" 74 | 75 | if d != want { 76 | t.Fatalf("wrong data in sink. want=%q, have=%q", want, d) 77 | } 78 | case <-time.After(1 * time.Second): 79 | t.Fatalf("ch has NO data") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/batches/ui/tty.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/creack/goselect" 7 | ) 8 | 9 | func HasInput(fd uintptr, timeout time.Duration) (bool, error) { 10 | rfds := &goselect.FDSet{} 11 | rfds.Zero() 12 | rfds.Set(fd) 13 | 14 | if err := goselect.Select(1, rfds, nil, nil, timeout); err != nil { 15 | return false, err 16 | } 17 | 18 | return rfds.IsSet(fd), nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/batches/util/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "util", 5 | srcs = ["repo.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/batches/util", 7 | visibility = ["//:__subpackages__"], 8 | deps = ["@com_github_sourcegraph_sourcegraph_lib//batches/template"], 9 | ) 10 | -------------------------------------------------------------------------------- /internal/batches/util/repo.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "strings" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/batches/template" 9 | ) 10 | 11 | // NewTemplatingRepo transforms a given *graphql.Repository into a 12 | // template.Repository. 13 | func NewTemplatingRepo(repoName string, branch string, fileMatches map[string]bool) template.Repository { 14 | matches := make([]string, 0, len(fileMatches)) 15 | for path := range fileMatches { 16 | matches = append(matches, path) 17 | } 18 | return template.Repository{ 19 | Name: repoName, 20 | Branch: branch, 21 | FileMatches: matches, 22 | } 23 | } 24 | 25 | func SlugForPathInRepo(repoName, commit, path string) string { 26 | name := repoName 27 | if path != "" { 28 | // Since path can contain os.PathSeparator or other characters that 29 | // don't translate well between Windows and Unix systems, we hash it. 30 | hash := sha256.Sum256([]byte(path)) 31 | name = name + "-" + base64.RawURLEncoding.EncodeToString(hash[:32]) 32 | } 33 | return strings.ReplaceAll(name, "/", "-") + "-" + commit 34 | } 35 | 36 | func SlugForRepo(repoName, commit string) string { 37 | return strings.ReplaceAll(repoName, "/", "-") + "-" + commit 38 | } 39 | 40 | func EnsureRefPrefix(ref string) string { 41 | if strings.HasPrefix(ref, "refs/heads/") { 42 | return ref 43 | } 44 | return "refs/heads/" + ref 45 | } 46 | -------------------------------------------------------------------------------- /internal/batches/watchdog/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "watchdog", 5 | srcs = ["watchdog.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/batches/watchdog", 7 | visibility = ["//:__subpackages__"], 8 | deps = ["@com_github_derision_test_glock//:glock"], 9 | ) 10 | 11 | go_test( 12 | name = "watchdog_test", 13 | srcs = ["watchdog_test.go"], 14 | embed = [":watchdog"], 15 | deps = ["@com_github_derision_test_glock//:glock"], 16 | ) 17 | -------------------------------------------------------------------------------- /internal/batches/watchdog/watchdog.go: -------------------------------------------------------------------------------- 1 | package watchdog 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/derision-test/glock" 7 | ) 8 | 9 | type WatchDog struct { 10 | ticker glock.Ticker 11 | callback func() 12 | done chan struct{} 13 | } 14 | 15 | func New(interval time.Duration, callback func()) *WatchDog { 16 | ticker := glock.NewRealTicker(interval) 17 | return &WatchDog{ 18 | ticker: ticker, 19 | callback: callback, 20 | done: make(chan struct{}, 1), 21 | } 22 | } 23 | 24 | func (w *WatchDog) Stop() { 25 | close(w.done) 26 | } 27 | 28 | func (w *WatchDog) Start() { 29 | for { 30 | select { 31 | case <-w.ticker.Chan(): 32 | go w.callback() 33 | case <-w.done: 34 | w.ticker.Stop() 35 | return 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/batches/watchdog/watchdog_test.go: -------------------------------------------------------------------------------- 1 | package watchdog 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | "github.com/derision-test/glock" 10 | ) 11 | 12 | func TestWatchDog(t *testing.T) { 13 | ticker := glock.NewMockTicker(5 * time.Minute) 14 | var count uint32 15 | expectedCount := 100 16 | var wg sync.WaitGroup 17 | 18 | mockCallback := func() { 19 | atomic.AddUint32(&count, 1) 20 | wg.Done() 21 | } 22 | 23 | w := &WatchDog{ 24 | ticker: ticker, 25 | callback: mockCallback, 26 | done: make(chan struct{}, 1), 27 | } 28 | 29 | go w.Start() 30 | for i := 0; i < expectedCount; i++ { 31 | wg.Add(1) 32 | ticker.BlockingAdvance(5 * time.Minute) 33 | } 34 | wg.Wait() 35 | w.Stop() 36 | 37 | if count != uint32(expectedCount) { 38 | t.Errorf("expected mock callback to be called %d times, got %d", expectedCount, count) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/batches/workspace/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "workspace", 5 | srcs = [ 6 | "bind_workspace.go", 7 | "executor_workspace.go", 8 | "git.go", 9 | "volume_workspace.go", 10 | "workspace.go", 11 | ], 12 | importpath = "github.com/sourcegraph/src-cli/internal/batches/workspace", 13 | visibility = ["//:__subpackages__"], 14 | deps = [ 15 | "//internal/batches/docker", 16 | "//internal/batches/graphql", 17 | "//internal/batches/repozip", 18 | "//internal/batches/util", 19 | "//internal/exec", 20 | "//internal/version", 21 | "@com_github_sourcegraph_sourcegraph_lib//batches", 22 | "@com_github_sourcegraph_sourcegraph_lib//errors", 23 | ], 24 | ) 25 | 26 | go_test( 27 | name = "workspace_test", 28 | srcs = [ 29 | "bind_workspace_nonwin_test.go", 30 | "bind_workspace_test.go", 31 | "bind_workspace_windows_test.go", 32 | "main_test.go", 33 | "volume_workspace_test.go", 34 | "workspace_test.go", 35 | ], 36 | embed = [":workspace"], 37 | deps = [ 38 | "//internal/batches/docker", 39 | "//internal/batches/graphql", 40 | "//internal/batches/mock", 41 | "//internal/batches/repozip", 42 | "//internal/exec/expect", 43 | "@com_github_google_go_cmp//cmp", 44 | "@com_github_sourcegraph_sourcegraph_lib//batches", 45 | "@com_github_sourcegraph_sourcegraph_lib//errors", 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /internal/batches/workspace/bind_workspace_nonwin_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package workspace 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/sourcegraph/sourcegraph/lib/errors" 12 | ) 13 | 14 | func TestMkdirAllStatError(t *testing.T) { 15 | // This test can't be trivially reproduced on Windows, so we just won't run 16 | // it there. 17 | 18 | // Create a shared workspace. 19 | base := mustCreateWorkspace(t) 20 | 21 | // We'll create a directory and a file within it, remove the execute bit on 22 | // the directory, and then stat() the file to cause a failure. 23 | if err := os.MkdirAll(filepath.Join(base, "locked"), 0700); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | f, err := os.Create(filepath.Join(base, "locked", "file")) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | f.Close() 32 | 33 | if err := os.Chmod(filepath.Join(base, "locked"), 0600); err != nil { 34 | t.Fatal(err) 35 | } 36 | // Add the execute bit back to the directory so "base" can be cleaned up 37 | t.Cleanup(func() { 38 | if err := os.Chmod(filepath.Join(base, "locked"), 0700); err != nil { 39 | t.Fatal(err) 40 | } 41 | }) 42 | 43 | err = mkdirAll(base, "locked/file", 0750) 44 | if err == nil { 45 | t.Errorf("unexpected nil error") 46 | } else if _, ok := err.(errPathExistsAsFile); ok { 47 | t.Errorf("unexpected error of type %T: %v", err, err) 48 | } 49 | } 50 | 51 | func mustHavePerm(t *testing.T, path string, want os.FileMode) error { 52 | t.Helper() 53 | 54 | if have := mustGetPerm(t, path); have != want { 55 | return errors.Errorf("unexpected permissions: have=%o want=%o", have, want) 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/batches/workspace/bind_workspace_windows_test.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/sourcegraph/sourcegraph/lib/errors" 8 | ) 9 | 10 | func mustHavePerm(t *testing.T, path string, want os.FileMode) error { 11 | t.Helper() 12 | 13 | have := mustGetPerm(t, path) 14 | 15 | // Go maps Windows file attributes onto Unix permissions in a fairly trivial 16 | // way: readonly files will be 0444, normal files will be 0666, and 17 | // directories will have 0111 or-ed onto that value. The end. Source: 18 | // https://sourcegraph.com/github.com/golang/go@fd841f65368906923e287afab91857043036459d/-/blob/src/os/types_windows.go#L112-134 19 | if want&0222 != 0 { 20 | want = 0666 21 | } else { 22 | want = 0444 23 | } 24 | if isDir(t, path) { 25 | want = want | 0111 26 | } 27 | 28 | if have != want { 29 | return errors.Errorf("unexpected permissions: have=%o want=%o", have, want) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/batches/workspace/executor_workspace.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "context" 5 | 6 | batcheslib "github.com/sourcegraph/sourcegraph/lib/batches" 7 | 8 | "github.com/sourcegraph/src-cli/internal/batches/graphql" 9 | "github.com/sourcegraph/src-cli/internal/batches/repozip" 10 | ) 11 | 12 | func NewExecutorWorkspaceCreator(tempDir, repoDir string) Creator { 13 | return &executorWorkspaceCreator{ 14 | TempDir: tempDir, 15 | RepoDir: repoDir, 16 | } 17 | } 18 | 19 | type executorWorkspaceCreator struct { 20 | TempDir string 21 | RepoDir string 22 | } 23 | 24 | var _ Creator = &executorWorkspaceCreator{} 25 | 26 | func (wc *executorWorkspaceCreator) Create(ctx context.Context, repo *graphql.Repository, steps []batcheslib.Step, archive repozip.Archive) (Workspace, error) { 27 | return &dockerBindExecutorWorkspace{ 28 | dockerBindWorkspace: dockerBindWorkspace{ 29 | tempDir: wc.TempDir, 30 | dir: wc.RepoDir, 31 | }, 32 | }, nil 33 | } 34 | 35 | // dockerBindExecutorWorkspace implements a workspace that operates on the host FS 36 | // and is mounted into the docker containers using a bind mount in the end. 37 | // It is based on the dockerBindWorkspace implementation, but does no cleanup 38 | // as that's handled by the executor, and we want to honor it's `keepWorkspaces` 39 | // setting. 40 | type dockerBindExecutorWorkspace struct { 41 | dockerBindWorkspace 42 | } 43 | 44 | var _ Workspace = &dockerBindExecutorWorkspace{} 45 | 46 | func (w *dockerBindExecutorWorkspace) Close(ctx context.Context) error { 47 | // Nothing to do here, executor cleanup will handle this. 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/batches/workspace/git.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/sourcegraph/sourcegraph/lib/errors" 9 | ) 10 | 11 | func runGitCmd(ctx context.Context, dir string, args ...string) ([]byte, error) { 12 | cmd := exec.CommandContext(ctx, "git", args...) 13 | cmd.Env = []string{ 14 | // Don't use the system wide git config. 15 | "GIT_CONFIG_NOSYSTEM=1", 16 | // And also not any other, because they can mess up output, change defaults, .. which can do unexpected things. 17 | "GIT_CONFIG=/dev/null", 18 | // Don't ask interactively for credentials. 19 | "GIT_TERMINAL_PROMPT=0", 20 | // Set user.name and user.email in the local repository. The user name and 21 | // e-mail will eventually be ignored anyway, since we're just using the Git 22 | // repository to generate diffs, but we don't want git to generate alarming 23 | // looking warnings. 24 | "GIT_AUTHOR_NAME=Sourcegraph", 25 | "GIT_AUTHOR_EMAIL=batch-changes@sourcegraph.com", 26 | "GIT_COMMITTER_NAME=Sourcegraph", 27 | "GIT_COMMITTER_EMAIL=batch-changes@sourcegraph.com", 28 | } 29 | cmd.Dir = dir 30 | out, err := cmd.Output() 31 | if err != nil { 32 | if exitErr, ok := err.(*exec.ExitError); ok { 33 | return out, errors.Wrapf(err, "'git %s' failed: %s", strings.Join(args, " "), string(exitErr.Stderr)) 34 | } 35 | return out, errors.Wrapf(err, "'git %s' failed: %s", strings.Join(args, " "), string(out)) 36 | } 37 | return out, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/batches/workspace/main_test.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/sourcegraph/src-cli/internal/exec/expect" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | code := expect.Handle(m) 12 | os.Exit(code) 13 | } 14 | -------------------------------------------------------------------------------- /internal/cmderrors/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "cmderrors", 5 | srcs = ["errors.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/cmderrors", 7 | visibility = ["//:__subpackages__"], 8 | deps = ["@com_github_sourcegraph_sourcegraph_lib//errors"], 9 | ) 10 | -------------------------------------------------------------------------------- /internal/cmderrors/errors.go: -------------------------------------------------------------------------------- 1 | package cmderrors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sourcegraph/sourcegraph/lib/errors" 7 | ) 8 | 9 | // UsageError is an error type that subcommands can return in order to signal 10 | // that a usage error has occurred. 11 | type UsageError struct { 12 | error 13 | } 14 | 15 | func Usage(msg string) *UsageError { 16 | return &UsageError{errors.New(msg)} 17 | } 18 | 19 | func Usagef(f string, args ...interface{}) *UsageError { 20 | return &UsageError{fmt.Errorf(f, args...)} 21 | } 22 | 23 | func ExitCode(code int, err error) *ExitCodeError { 24 | return &ExitCodeError{error: err, exitCode: code} 25 | } 26 | 27 | // ExitCodeError is an error type that subcommands can return in order to 28 | // specify the exact exit code. 29 | type ExitCodeError struct { 30 | error 31 | exitCode int 32 | } 33 | 34 | func (e *ExitCodeError) HasError() bool { return e.error != nil } 35 | func (e *ExitCodeError) Code() int { return e.exitCode } 36 | 37 | func (e *ExitCodeError) Error() string { 38 | if e.error != nil { 39 | return fmt.Sprintf("%s (exit code: %d)", e.error, e.exitCode) 40 | } 41 | return fmt.Sprintf("exit code: %d", e.exitCode) 42 | } 43 | 44 | const ( 45 | GraphqlErrorsExitCode = 2 46 | ) 47 | 48 | var ExitCode1 = &ExitCodeError{exitCode: 1} 49 | -------------------------------------------------------------------------------- /internal/codeintel/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "codeintel", 5 | srcs = [ 6 | "gitutil.go", 7 | "sanitation.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/codeintel", 10 | visibility = ["//:__subpackages__"], 11 | ) 12 | 13 | go_test( 14 | name = "codeintel_test", 15 | srcs = [ 16 | "gitutil_test.go", 17 | "sanitation_test.go", 18 | ], 19 | embed = [":codeintel"], 20 | ) 21 | -------------------------------------------------------------------------------- /internal/codeintel/gitutil.go: -------------------------------------------------------------------------------- 1 | package codeintel 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // InferRepo gets a Sourcegraph-friendly repo name from the git clone enclosing the working dir. 12 | func InferRepo() (string, error) { 13 | remoteURL, err := runGitCommand("remote", "get-url", "origin") 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | return parseRemote(remoteURL) 19 | } 20 | 21 | // parseRemote converts a git origin url into a Sourcegraph-friendly repo name. 22 | func parseRemote(remoteURL string) (string, error) { 23 | // e.g., git@github.com:sourcegraph/src-cli.git 24 | if strings.HasPrefix(remoteURL, "git@") { 25 | if parts := strings.Split(remoteURL, ":"); len(parts) == 2 { 26 | return strings.Join([]string{ 27 | strings.TrimPrefix(parts[0], "git@"), 28 | strings.TrimSuffix(parts[1], ".git"), 29 | }, "/"), nil 30 | } 31 | } 32 | 33 | // e.g., https://github.com/sourcegraph/src-cli.git 34 | if url, err := url.Parse(remoteURL); err == nil { 35 | return url.Hostname() + strings.TrimSuffix(url.Path, ".git"), nil 36 | } 37 | 38 | return "", fmt.Errorf("unrecognized remote URL: %s", remoteURL) 39 | } 40 | 41 | // InferCommit gets a 40-character rev hash from the git clone enclosing the working dir. 42 | func InferCommit() (string, error) { 43 | return runGitCommand("rev-parse", "HEAD") 44 | } 45 | 46 | // InferRoot gets the path relative to the root of the git clone enclosing the given file path. 47 | func InferRoot(file string) (string, error) { 48 | topLevel, err := runGitCommand("rev-parse", "--show-toplevel") 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | absoluteFile, err := filepath.Abs(file) 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | relative, err := filepath.Rel(topLevel, absoluteFile) 59 | if err != nil { 60 | return "", err 61 | } 62 | 63 | return filepath.Dir(relative), nil 64 | } 65 | 66 | // runGitCommand runs a git command and trims all leading/trailing whitespace from the output. 67 | func runGitCommand(args ...string) (string, error) { 68 | output, err := exec.Command("git", args...).CombinedOutput() 69 | if err != nil { 70 | return "", fmt.Errorf("failed to run git command: %s\n%s", err, output) 71 | } 72 | 73 | return strings.TrimSpace(string(output)), nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/codeintel/gitutil_test.go: -------------------------------------------------------------------------------- 1 | package codeintel 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestInferRepo(t *testing.T) { 11 | cur, err := os.Getwd() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | defer os.Chdir(cur) 16 | 17 | tempDir := t.TempDir() 18 | err = os.Chdir(tempDir) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | _, err = runGitCommand("init") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | want := "github.com/a/b" 29 | 30 | _, err = runGitCommand("remote", "add", "origin", want) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | got, err := InferRepo() 35 | if err != nil { 36 | t.Fatalf("unexpected error inferring repo: %s", err) 37 | } 38 | 39 | if got != want { 40 | t.Errorf("unexpected remote repo. want=%q have=%q", want, got) 41 | } 42 | } 43 | 44 | func TestParseRemote(t *testing.T) { 45 | testCases := map[string]string{ 46 | "git@github.com:sourcegraph/src-cli.git": "github.com/sourcegraph/src-cli", 47 | "https://github.com/sourcegraph/src-cli": "github.com/sourcegraph/src-cli", 48 | } 49 | 50 | for input, expectedOutput := range testCases { 51 | t.Run(fmt.Sprintf("input=%q", input), func(t *testing.T) { 52 | output, err := parseRemote(input) 53 | if err != nil { 54 | t.Fatalf("unexpected error parsing remote: %s", err) 55 | } 56 | 57 | if output != expectedOutput { 58 | t.Errorf("unexpected repo name. want=%q have=%q", expectedOutput, output) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestInferRoot(t *testing.T) { 65 | testCases := map[string]string{ 66 | "gitutil.go": filepath.Join("internal", "codeintel"), 67 | "../../cmd/src/code_intel_upload.go": filepath.Join("cmd", "src"), 68 | "../../README.md": ".", 69 | } 70 | 71 | for input, expectedOutput := range testCases { 72 | t.Run(fmt.Sprintf("input=%q", input), func(t *testing.T) { 73 | root, err := InferRoot(input) 74 | if err != nil { 75 | t.Fatalf("unexpected error inferring root: %s", err) 76 | } 77 | 78 | if root != expectedOutput { 79 | t.Errorf("unexpected remote root. want=%q have=%q", expectedOutput, root) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/codeintel/sanitation.go: -------------------------------------------------------------------------------- 1 | package codeintel 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // SanitizeRoot removes redundant paths from the given root and replaces 9 | // references to the root of th repo with an empty string. 10 | func SanitizeRoot(root string) string { 11 | if root = filepath.Clean(root); root == "." || (root != "" && os.IsPathSeparator(root[0])) { 12 | return "" 13 | } 14 | return root 15 | } 16 | -------------------------------------------------------------------------------- /internal/codeintel/sanitation_test.go: -------------------------------------------------------------------------------- 1 | package codeintel 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestSanitizeRoot(t *testing.T) { 10 | testCases := map[string]string{ 11 | "": "", 12 | ".": "", 13 | "/": "", 14 | "foo/../bar": "bar", 15 | "./bar/baz/../bonk": filepath.Join("bar", "bonk"), 16 | } 17 | 18 | for input, expectedOutput := range testCases { 19 | t.Run(fmt.Sprintf("input=%q", input), func(t *testing.T) { 20 | if SanitizeRoot(input) != expectedOutput { 21 | t.Errorf("unexpected root. want=%q have=%q", expectedOutput, SanitizeRoot(input)) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/exec/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "exec", 5 | srcs = [ 6 | "exec.go", 7 | "middleware.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/exec", 10 | visibility = ["//:__subpackages__"], 11 | ) 12 | -------------------------------------------------------------------------------- /internal/exec/exec.go: -------------------------------------------------------------------------------- 1 | // Package exec provides wrapped implementations of os/exec's Command and 2 | // CommandContext functions that allow for command creation to be overridden, 3 | // thereby allowing commands to be mocked. 4 | package exec 5 | 6 | import ( 7 | "context" 8 | goexec "os/exec" 9 | ) 10 | 11 | // CmdCreator instances are used to create commands. os/exec.CommandContext is a 12 | // valid CmdCreator. 13 | type CmdCreator func(context.Context, string, ...string) *goexec.Cmd 14 | 15 | // creator is the singleton used to create a new command. 16 | var creator CmdCreator = goexec.CommandContext 17 | 18 | // Command wraps os/exec.Command, and implements the same behaviour. 19 | func Command(name string, arg ...string) *goexec.Cmd { 20 | return CommandContext(context.TODO(), name, arg...) 21 | } 22 | 23 | // CommandContext wraps os/exec.CommandContext, and implements the same 24 | // behaviour. 25 | func CommandContext(ctx context.Context, name string, arg ...string) *goexec.Cmd { 26 | // TODO: if we add global logging infrastructure to cmd/src, we could 27 | // leverage it here to log all commands that are executed in an appropriate 28 | // verbose mode. 29 | 30 | return creator(ctx, name, arg...) 31 | } 32 | -------------------------------------------------------------------------------- /internal/exec/expect/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "expect", 5 | srcs = ["expect.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/exec/expect", 7 | visibility = ["//:__subpackages__"], 8 | deps = [ 9 | "//internal/exec", 10 | "@com_github_gobwas_glob//:glob", 11 | "@com_github_google_go_cmp//cmp", 12 | "@com_github_sourcegraph_sourcegraph_lib//errors", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /internal/exec/middleware.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "context" 5 | goexec "os/exec" 6 | ) 7 | 8 | // CmdCreatorMiddleware creates *exec.Cmd instances that delegate command 9 | // creation to a provided callback. 10 | type CmdCreatorMiddleware struct{ previous CmdCreator } 11 | 12 | // NewMiddleware adds a middleware to the command creation stack. 13 | func NewMiddleware(mock func(context.Context, CmdCreator, string, ...string) *goexec.Cmd) CmdCreatorMiddleware { 14 | mc := CmdCreatorMiddleware{previous: creator} 15 | creator = func(ctx context.Context, name string, arg ...string) *goexec.Cmd { 16 | return mock(ctx, mc.previous, name, arg...) 17 | } 18 | return mc 19 | } 20 | 21 | // Remove removes the command creation middleware from the stack. 22 | func (mc CmdCreatorMiddleware) Remove() { 23 | creator = mc.previous 24 | } 25 | -------------------------------------------------------------------------------- /internal/instancehealth/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "instancehealth", 5 | srcs = [ 6 | "checks.go", 7 | "summary.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/instancehealth", 10 | visibility = ["//:__subpackages__"], 11 | deps = [ 12 | "//internal/api", 13 | "@com_github_sourcegraph_sourcegraph_lib//errors", 14 | "@com_github_sourcegraph_sourcegraph_lib//output", 15 | ], 16 | ) 17 | 18 | go_test( 19 | name = "instancehealth_test", 20 | srcs = ["checks_test.go"], 21 | embed = [":instancehealth"], 22 | deps = [ 23 | "@com_github_sourcegraph_sourcegraph_lib//output", 24 | "@com_github_stretchr_testify//assert", 25 | "@com_github_stretchr_testify//require", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /internal/lazyregexp/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "lazyregexp", 5 | srcs = ["lazyregexp.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/lazyregexp", 7 | visibility = ["//:__subpackages__"], 8 | deps = ["@com_github_grafana_regexp//:regexp"], 9 | ) 10 | -------------------------------------------------------------------------------- /internal/pgdump/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "pgdump", 5 | srcs = [ 6 | "extensions.go", 7 | "pgdump.go", 8 | ], 9 | importpath = "github.com/sourcegraph/src-cli/internal/pgdump", 10 | visibility = ["//:__subpackages__"], 11 | deps = ["@com_github_sourcegraph_sourcegraph_lib//errors"], 12 | ) 13 | 14 | go_test( 15 | name = "pgdump_test", 16 | srcs = ["extensions_test.go"], 17 | embed = [":pgdump"], 18 | deps = [ 19 | "@com_github_hexops_autogold//:autogold", 20 | "@com_github_stretchr_testify//assert", 21 | "@com_github_stretchr_testify//require", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /internal/pgdump/extensions_test.go: -------------------------------------------------------------------------------- 1 | package pgdump 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/hexops/autogold" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestPartialCopyWithoutExtensions(t *testing.T) { 17 | if runtime.GOOS == "windows" { 18 | t.Skip("Test doesn't work on Windows of weirdness with t.TempDir() handling") 19 | } 20 | 21 | // Create test data - there is no stdlib in-memory io.ReadSeeker implementation 22 | src, err := os.Create(filepath.Join(t.TempDir(), t.Name())) 23 | require.NoError(t, err) 24 | _, err = src.WriteString(`-- Some comment 25 | 26 | CREATE EXTENSION foobar 27 | 28 | COMMENT ON EXTENSION barbaz 29 | 30 | CREATE TYPE asdf 31 | 32 | CREATE TABLE robert ( 33 | ... 34 | ) 35 | 36 | CREATE TABLE bobhead ( 37 | ... 38 | )`) 39 | require.NoError(t, err) 40 | _, err = src.Seek(0, io.SeekStart) 41 | require.NoError(t, err) 42 | 43 | // Set up target to assert against 44 | var dst bytes.Buffer 45 | 46 | // Perform partial copy 47 | _, err = PartialCopyWithoutExtensions(&dst, src, func(i int64) {}) 48 | assert.NoError(t, err) 49 | 50 | // Copy rest of contents 51 | _, err = io.Copy(&dst, src) 52 | assert.NoError(t, err) 53 | 54 | // Assert contents (update with -update) 55 | autogold.Want("partial-copy-without-extensions", `-- Some comment 56 | 57 | CREATE EXTENSION foobar 58 | 59 | -- COMMENT ON EXTENSION barbaz 60 | 61 | CREATE TYPE asdf 62 | 63 | CREATE TABLE robert ( 64 | ... 65 | ) 66 | 67 | CREATE TABLE bobhead ( 68 | ... 69 | )`).Equal(t, dst.String()) 70 | } 71 | -------------------------------------------------------------------------------- /internal/scout/constants.go: -------------------------------------------------------------------------------- 1 | package scout 2 | 3 | const ( 4 | ABillion float64 = 1_000_000_000 5 | EmojiFingerPointRight = "👉" 6 | FlashingLightEmoji = "🚨" 7 | SuccessEmoji = "✅" 8 | WarningSign = "⚠️ " // why does this need an extra space to align?!?! 9 | HEALTHY = "HEALTHY" 10 | WARNING = "WARNING" 11 | DANGER = "DANGER" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/scout/helpers.go: -------------------------------------------------------------------------------- 1 | package scout 2 | 3 | // contains checks if a string slice contains a given value. 4 | func Contains(slice []string, value string) bool { 5 | for _, v := range slice { 6 | if v == value { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | 13 | // getPercentage calculates the percentage of x in relation to y. 14 | func GetPercentage(x, y float64) float64 { 15 | if x == 0 { 16 | return 0 17 | } 18 | 19 | if y == 0 { 20 | return 0 21 | } 22 | 23 | return x * 100 / y 24 | } 25 | -------------------------------------------------------------------------------- /internal/scout/kube/kube_test.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | func TestAcceptedFileSystem(t *testing.T) { 11 | cases := []struct { 12 | name string 13 | filesystem string 14 | want bool 15 | }{ 16 | { 17 | name: "should return true if filesystem matches 'matched' regular expression", 18 | filesystem: "/dev/sda", 19 | want: true, 20 | }, 21 | { 22 | name: "should return false if filesystem doesn't match 'matched' regular expression", 23 | filesystem: "/dev/sda1", 24 | want: false, 25 | }, 26 | } 27 | 28 | for _, tc := range cases { 29 | tc := tc 30 | t.Run(tc.name, func(t *testing.T) { 31 | got := acceptedFileSystem(tc.filesystem) 32 | if got != tc.want { 33 | t.Errorf("got %v want %v", got, tc.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestGetPod(t *testing.T) { 40 | cases := []struct { 41 | name string 42 | podList []corev1.Pod 43 | wantPod string 44 | }{ 45 | { 46 | name: "should return correct pod", 47 | podList: []corev1.Pod{ 48 | *testPod("sg", "soucegraph-frontend-0", "sourcegraph-frontend"), 49 | *testPod("sg", "gitserver-0", "gitserver"), 50 | *testPod("sg", "indexed-search-0", "indexed-search"), 51 | }, 52 | wantPod: "gitserver-0", 53 | }, 54 | { 55 | name: "should return empty pod if pod not found", 56 | podList: []corev1.Pod{ 57 | *testPod("sg", "soucegraph-frontend-0", "sourcegraph-frontend"), 58 | *testPod("sg", "indexed-search-0", "indexed-search"), 59 | }, 60 | wantPod: "", 61 | }, 62 | } 63 | 64 | for _, tc := range cases { 65 | tc := tc 66 | t.Run(tc.name, func(t *testing.T) { 67 | got, _ := GetPod("gitserver-0", tc.podList) 68 | gotPod := got.Name 69 | 70 | if gotPod != tc.wantPod { 71 | t.Errorf("want pod %s, got pod %s", tc.wantPod, gotPod) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func testPod(namespace, podName, containerName string) *corev1.Pod { 78 | return &corev1.Pod{ 79 | ObjectMeta: metav1.ObjectMeta{ 80 | Namespace: namespace, 81 | Name: podName, 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/scout/resource/resource_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestGetMemUnits(t *testing.T) { 9 | cases := []struct { 10 | name string 11 | param int64 12 | wantUnit string 13 | wantValue int64 14 | wantError error 15 | }{ 16 | { 17 | name: "convert bytes below a million to KB", 18 | param: 999999, 19 | wantUnit: "KB", 20 | wantValue: 999999, 21 | wantError: nil, 22 | }, 23 | { 24 | name: "convert bytes below a billion to MB", 25 | param: 999999999, 26 | wantUnit: "MB", 27 | wantValue: 999, 28 | wantError: nil, 29 | }, 30 | { 31 | name: "convert bytes above a billion to GB", 32 | param: 12999999900, 33 | wantUnit: "GB", 34 | wantValue: 12, 35 | wantError: nil, 36 | }, 37 | { 38 | name: "return error for a negative number", 39 | param: -300, 40 | wantUnit: "", 41 | wantValue: -300, 42 | wantError: fmt.Errorf("invalid memory value: %d", -300), 43 | }, 44 | } 45 | 46 | for _, tc := range cases { 47 | tc := tc 48 | t.Run(tc.name, func(t *testing.T) { 49 | gotUnit, gotValue, gotError := getMemUnits(tc.param) 50 | 51 | if gotUnit != tc.wantUnit { 52 | t.Errorf("got %s want %s", gotUnit, tc.wantUnit) 53 | } 54 | 55 | if gotValue != tc.wantValue { 56 | t.Errorf("got %v want %v", gotValue, tc.wantValue) 57 | } 58 | 59 | if gotError == nil && tc.wantError != nil { 60 | t.Error("got nil want error") 61 | } 62 | 63 | if gotError != nil && tc.wantError == nil { 64 | t.Error("got error want nil") 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/scout/types.go: -------------------------------------------------------------------------------- 1 | package scout 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/resource" 5 | "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/rest" 7 | metricsv "k8s.io/metrics/pkg/client/clientset/versioned" 8 | ) 9 | 10 | type Config struct { 11 | Namespace string 12 | Pod string 13 | Output string 14 | Warnings bool 15 | RestConfig *rest.Config 16 | K8sClient *kubernetes.Clientset 17 | MetricsClient *metricsv.Clientset 18 | } 19 | 20 | type ContainerMetrics struct { 21 | PodName string 22 | Limits map[string]Resources 23 | } 24 | 25 | type Resources struct { 26 | Cpu *resource.Quantity 27 | Memory *resource.Quantity 28 | Storage *resource.Quantity 29 | } 30 | 31 | type UsageStats struct { 32 | ContainerName string 33 | CpuCores *resource.Quantity 34 | Memory *resource.Quantity 35 | Storage *resource.Quantity 36 | CpuUsage float64 37 | MemoryUsage float64 38 | StorageUsage float64 39 | } 40 | 41 | type Advice struct { 42 | Kind string 43 | Msg string 44 | } 45 | -------------------------------------------------------------------------------- /internal/scout/usage/usage.go: -------------------------------------------------------------------------------- 1 | package usage 2 | 3 | import ( 4 | "github.com/sourcegraph/src-cli/internal/scout" 5 | ) 6 | 7 | type Option = func(config *scout.Config) 8 | 9 | func WithNamespace(namespace string) Option { 10 | return func(config *scout.Config) { 11 | config.Namespace = namespace 12 | } 13 | } 14 | 15 | func WithPod(podname string) Option { 16 | return func(config *scout.Config) { 17 | config.Pod = podname 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/servegit/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "servegit", 5 | srcs = ["serve.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/servegit", 7 | visibility = ["//:__subpackages__"], 8 | deps = [ 9 | "@com_github_sourcegraph_sourcegraph_lib//errors", 10 | "@com_github_sourcegraph_sourcegraph_lib//gitservice", 11 | ], 12 | ) 13 | 14 | go_test( 15 | name = "servegit_test", 16 | srcs = ["serve_test.go"], 17 | embed = [":servegit"], 18 | deps = [ 19 | "@com_github_google_go_cmp//cmp", 20 | "@com_github_google_go_cmp//cmp/cmpopts", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /internal/streaming/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "streaming", 5 | srcs = [ 6 | "api.go", 7 | "client.go", 8 | "events.go", 9 | "search.go", 10 | "writer.go", 11 | ], 12 | importpath = "github.com/sourcegraph/src-cli/internal/streaming", 13 | visibility = ["//:__subpackages__"], 14 | deps = [ 15 | "//internal/api", 16 | "@com_github_sourcegraph_sourcegraph_lib//errors", 17 | ], 18 | ) 19 | 20 | go_test( 21 | name = "streaming_test", 22 | srcs = ["client_test.go"], 23 | embed = [":streaming"], 24 | deps = ["@com_github_google_go_cmp//cmp"], 25 | ) 26 | -------------------------------------------------------------------------------- /internal/streaming/search.go: -------------------------------------------------------------------------------- 1 | package streaming 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/sourcegraph/src-cli/internal/api" 11 | ) 12 | 13 | // Opts contains the search options supported by Search. 14 | type Opts struct { 15 | Display int 16 | Trace bool 17 | Json bool 18 | } 19 | 20 | // Search calls the streaming search endpoint and uses decoder to decode the 21 | // response body. 22 | func Search(query string, opts Opts, client api.Client, decoder Decoder) error { 23 | // Create request. 24 | req, err := client.NewHTTPRequest(context.Background(), "GET", ".api/search/stream?q="+url.QueryEscape(query), nil) 25 | if err != nil { 26 | return err 27 | } 28 | req.Header.Set("Accept", "text/event-stream") 29 | if opts.Display >= 0 { 30 | q := req.URL.Query() 31 | q.Add("display", strconv.Itoa(opts.Display)) 32 | req.URL.RawQuery = q.Encode() 33 | } 34 | 35 | { 36 | // Consume chunk matches for streaming search. 37 | q := req.URL.Query() 38 | q.Add("cm", "t") 39 | req.URL.RawQuery = q.Encode() 40 | } 41 | 42 | // Send request. 43 | resp, err := client.Do(req) 44 | if err != nil { 45 | return fmt.Errorf("error sending request: %w", err) 46 | } 47 | defer resp.Body.Close() 48 | 49 | // Process response. 50 | err = decoder.ReadAll(resp.Body) 51 | if err != nil { 52 | return fmt.Errorf("error during decoding: %w", err) 53 | } 54 | 55 | // Output trace. 56 | if opts.Trace { 57 | _, err = fmt.Fprintf(os.Stderr, "\nx-trace: %s\n", resp.Header.Get("x-trace")) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/users/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "users", 5 | srcs = ["admin.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/users", 7 | visibility = ["//:__subpackages__"], 8 | deps = [ 9 | "//internal/lazyregexp", 10 | "@com_github_json_iterator_go//:go", 11 | "@com_github_sourcegraph_sourcegraph_lib//errors", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /internal/validate/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "validate", 5 | srcs = ["validate.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/validate", 7 | visibility = ["//:__subpackages__"], 8 | ) 9 | -------------------------------------------------------------------------------- /internal/validate/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/src-cli/983280ce4bf15b75f7a685101261246c6906b8ae/internal/validate/README.md -------------------------------------------------------------------------------- /internal/validate/install/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "install", 5 | srcs = [ 6 | "config.go", 7 | "github.go", 8 | "insight.go", 9 | "install.go", 10 | ], 11 | importpath = "github.com/sourcegraph/src-cli/internal/validate/install", 12 | visibility = ["//:__subpackages__"], 13 | deps = [ 14 | "//internal/api", 15 | "//internal/validate", 16 | "@com_github_sourcegraph_sourcegraph_lib//errors", 17 | "@in_gopkg_yaml_v3//:yaml_v3", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /internal/validate/kube/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "kube", 5 | srcs = [ 6 | "aks.go", 7 | "eks.go", 8 | "gke.go", 9 | "kube.go", 10 | ], 11 | importpath = "github.com/sourcegraph/src-cli/internal/validate/kube", 12 | visibility = ["//:__subpackages__"], 13 | deps = [ 14 | "//internal/validate", 15 | "@com_github_aws_aws_sdk_go_v2_config//:config", 16 | "@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2", 17 | "@com_github_aws_aws_sdk_go_v2_service_ec2//types", 18 | "@com_github_aws_aws_sdk_go_v2_service_eks//:eks", 19 | "@com_github_aws_aws_sdk_go_v2_service_iam//:iam", 20 | "@com_github_sourcegraph_sourcegraph_lib//errors", 21 | "@io_k8s_api//core/v1:core", 22 | "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", 23 | "@io_k8s_client_go//kubernetes", 24 | "@io_k8s_client_go//kubernetes/scheme", 25 | "@io_k8s_client_go//rest", 26 | "@io_k8s_client_go//tools/clientcmd", 27 | "@io_k8s_client_go//tools/remotecommand", 28 | "@io_k8s_client_go//util/homedir", 29 | ], 30 | ) 31 | 32 | go_test( 33 | name = "kube_test", 34 | srcs = [ 35 | "eks_test.go", 36 | "kube_test.go", 37 | ], 38 | embed = [":kube"], 39 | deps = [ 40 | "//internal/validate", 41 | "@com_github_aws_aws_sdk_go_v2_service_ec2//types", 42 | "@com_github_aws_aws_sdk_go_v2_service_eks//:eks", 43 | "@com_github_aws_aws_sdk_go_v2_service_iam//:iam", 44 | "@com_github_aws_aws_sdk_go_v2_service_iam//types", 45 | "@io_k8s_api//core/v1:core", 46 | "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", 47 | ], 48 | ) 49 | -------------------------------------------------------------------------------- /internal/validate/kube/gke.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | 8 | "github.com/sourcegraph/src-cli/internal/validate" 9 | ) 10 | 11 | type ClusterInfo struct { 12 | ServiceType string 13 | ProjectId string 14 | Region string 15 | ClusterName string 16 | } 17 | 18 | func Gke() Option { 19 | return func(config *Config) { 20 | config.gke = true 21 | } 22 | } 23 | 24 | func GkeGcePersistentDiskCSIDrivers(ctx context.Context, config *Config) ([]validate.Result, error) { 25 | var results []validate.Result 26 | 27 | checkStorageClassesResults, err := validateStorageClasses(ctx, config) 28 | if err != nil { 29 | results = append(results, validate.Result{ 30 | Status: validate.Failure, 31 | Message: "GKE: could not check StorageClasses", 32 | }) 33 | return results, nil 34 | } 35 | 36 | results = append(results, checkStorageClassesResults...) 37 | return results, nil 38 | } 39 | 40 | // validateStorageClasses checks for GKE specific storageClasses: 41 | // 42 | // After the compute engine persistent disk CSI driver is enabled, 43 | // gke automatically installs the standard-rwo and the premium-rwo 44 | // storage classes. This function checks that those storage 45 | // classes exist on the cluster. 46 | // 47 | // Ref: shorturl.at/dnKV0 48 | func validateStorageClasses(ctx context.Context, config *Config) ([]validate.Result, error) { 49 | var results []validate.Result 50 | 51 | storageClient := config.clientSet.StorageV1() 52 | storageClasses, err := storageClient.StorageClasses().List(ctx, metav1.ListOptions{}) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | classes := 0 58 | for _, item := range storageClasses.Items { 59 | if item.Name == "premium-rwo" || item.Name == "standard-rwo" { 60 | classes += 1 61 | } 62 | } 63 | 64 | if classes == 2 { 65 | results = append(results, validate.Result{ 66 | Status: validate.Success, 67 | Message: "persistent volumes enabled: validated", 68 | }) 69 | 70 | return results, nil 71 | } 72 | 73 | results = append(results, validate.Result{ 74 | Status: validate.Failure, 75 | Message: "validate persistent volumes enabled: failed", 76 | }) 77 | 78 | return results, nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | var ( 4 | EmojiFingerPointRight = "👉" 5 | FailureEmoji = "🛑" 6 | FlashingLightEmoji = "🚨" 7 | HourglassEmoji = "⌛" 8 | SuccessEmoji = "✅" 9 | WarningSign = "⚠️ " // why does this need an extra space to align?!?! 10 | ) 11 | 12 | type Status string 13 | 14 | const ( 15 | Failure Status = "Failure" 16 | Warning Status = "Warning" 17 | Success Status = "Success" 18 | ) 19 | 20 | type Result struct { 21 | Status Status 22 | Message string 23 | } 24 | -------------------------------------------------------------------------------- /internal/version/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "version", 5 | srcs = ["version.go"], 6 | importpath = "github.com/sourcegraph/src-cli/internal/version", 7 | visibility = ["//:__subpackages__"], 8 | ) 9 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // DefaultBuildTag is the value BuildTag will be set to if this is not a release 4 | // build. 5 | const DefaultBuildTag = "dev" 6 | 7 | // BuildTag is the git tag at the time of build and is used to 8 | // denote the binary's current version. This value is supplied 9 | // as an ldflag at compile time by the GoReleaser action. 10 | var BuildTag = DefaultBuildTag 11 | -------------------------------------------------------------------------------- /npm-distribution/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src 3 | LICENSE 4 | README.md 5 | -------------------------------------------------------------------------------- /npm-distribution/copy-files.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const licensePath = path.join(__dirname, '..', 'LICENSE') 5 | const readmePath = path.join(__dirname, '..', 'README.md') 6 | 7 | fs.copyFileSync(licensePath, path.join(__dirname, 'LICENSE')) 8 | fs.copyFileSync(readmePath, path.join(__dirname, 'README.md')) 9 | -------------------------------------------------------------------------------- /npm-distribution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sourcegraph/src", 3 | "version": "0.0.0-dev", 4 | "description": "Sourcegraph CLI", 5 | "repository": "git@github.com:sourcegraph/src-cli.git", 6 | "author": "Code Intelligence at Sourcegraph", 7 | "license": "Apache-2.0", 8 | "scripts": { 9 | "install": "node install.js", 10 | "prepack": "node copy-files.js" 11 | }, 12 | "main": "src.js", 13 | "bin": { 14 | "src": "src.js" 15 | }, 16 | "keywords": [ 17 | "sourcegraph" 18 | ], 19 | "dependencies": { 20 | "tar": "^7.1.0" 21 | }, 22 | "devDependencies": { 23 | "@types/tar": "6.1.13" 24 | }, 25 | "private": false 26 | } 27 | -------------------------------------------------------------------------------- /npm-distribution/src.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const spawn = require("child_process").spawn; 5 | const executable = process.platform === 'win32' ? 'src.exe' : 'src'; 6 | spawn( 7 | path.join(__dirname, executable), 8 | process.argv.slice(2), 9 | {stdio: 'inherit'} 10 | ).on('close', process.exit) 11 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euf -o pipefail 4 | 5 | read -p 'Have you read DEVELOPMENT.md? [y/N] ' -n 1 -r 6 | echo 7 | case "$REPLY" in 8 | Y | y) ;; 9 | *) 10 | echo 'Please read the Releasing section of DEVELOPMENT.md before running this script.' 11 | exit 1 12 | ;; 13 | esac 14 | 15 | if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$'; then 16 | echo "\$VERSION is not in MAJOR.MINOR.PATCH format" 17 | exit 1 18 | fi 19 | 20 | # Create a new tag and push it, this will trigger the goreleaser workflow in .github/workflows/goreleaser.yml 21 | git tag "${VERSION}" -a -m "release v${VERSION}" 22 | # We use `--atomic` so that we push the tag and the commit if the commit was or wasn't pushed before 23 | git push --atomic origin main "${VERSION}" 24 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/renovate", 3 | "extends": [ 4 | "github>sourcegraph/renovate-config" 5 | ], 6 | "semanticCommits": false 7 | } 8 | --------------------------------------------------------------------------------