mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad1a3c39f | ||
|
|
d4a8a79628 | ||
|
|
007c9f9efe | ||
|
|
f61203ac1b | ||
|
|
f1db4ee378 | ||
|
|
366d63afdc | ||
|
|
c1551683a3 | ||
|
|
09aad6f8ea | ||
|
|
adb5a43810 | ||
|
|
cab039a9d8 | ||
|
|
97e80beceb | ||
|
|
1969423b5c | ||
|
|
4497ce1b84 | ||
|
|
5eee02bc40 | ||
|
|
2d50c6a6eb | ||
|
|
6f919fd675 | ||
|
|
fd1465ee38 | ||
|
|
00ec2c130d | ||
|
|
8eafa5adfe | ||
|
|
b604027205 | ||
|
|
2a19755804 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Force LF line endings for mock/test data files to ensure consistent
|
||||||
|
# behavior across platforms (Windows git autocrlf converts to CRLF otherwise)
|
||||||
|
mocks/** text eol=lf
|
||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -3,9 +3,5 @@ updates:
|
|||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
ignore:
|
|
||||||
- dependency-name: github.com/alecthomas/chroma
|
|
||||||
versions:
|
|
||||||
- 0.9.1
|
|
||||||
|
|||||||
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
@@ -1,46 +1,38 @@
|
|||||||
---
|
---
|
||||||
name: Go
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# TODO: is it possible to DRY out these jobs? Aside from `runs-on`, they are
|
lint:
|
||||||
# identical.
|
runs-on: ubuntu-latest
|
||||||
# See: https://github.com/actions/runner/issues/1182
|
|
||||||
build-linux:
|
|
||||||
runs-on: [ubuntu-latest]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- uses: actions/setup-go@v5
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: stable
|
||||||
- name: Set up Revive (linter)
|
- name: Install revive
|
||||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
run: go install github.com/mgechev/revive@latest
|
||||||
env:
|
- name: Lint
|
||||||
GO111MODULE: "off"
|
run: revive -exclude vendor/... ./...
|
||||||
- name: Build
|
- name: Vet
|
||||||
run: make build
|
run: go vet ./...
|
||||||
- name: Test
|
- name: Check formatting
|
||||||
run: make test
|
run: test -z "$(gofmt -l . | grep -v vendor/)"
|
||||||
|
|
||||||
build-osx:
|
test:
|
||||||
runs-on: [macos-latest]
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- uses: actions/setup-go@v5
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: stable
|
||||||
- name: Set up Revive (linter)
|
|
||||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
|
||||||
env:
|
|
||||||
GO111MODULE: "off"
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build
|
run: go build -mod vendor ./cmd/cheat
|
||||||
- name: Test
|
- name: Test
|
||||||
run: make test
|
run: go test ./...
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
|||||||
language: [go]
|
language: [go]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v3
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
19
.github/workflows/homebrew.yml
vendored
19
.github/workflows/homebrew.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: homebrew
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
homebrew:
|
|
||||||
name: Bump Homebrew formula
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: mislav/bump-homebrew-formula-action@v1
|
|
||||||
with:
|
|
||||||
# A PR will be sent to github.com/Homebrew/homebrew-core to update
|
|
||||||
# this formula:
|
|
||||||
formula-name: cheat
|
|
||||||
env:
|
|
||||||
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
|
||||||
@@ -85,7 +85,11 @@ The `cheat` command-line tool is organized into several key packages:
|
|||||||
- Writes to stdout or pager
|
- Writes to stdout or pager
|
||||||
- Handles text formatting and indentation
|
- Handles text formatting and indentation
|
||||||
|
|
||||||
6. **`internal/repo`**: Git repository management
|
6. **`internal/installer`**: First-run installer
|
||||||
|
- Prompts user for initial configuration choices
|
||||||
|
- Generates default `conf.yml` and downloads community cheatsheets
|
||||||
|
|
||||||
|
7. **`internal/repo`**: Git repository management
|
||||||
- Clones community cheatsheet repositories
|
- Clones community cheatsheet repositories
|
||||||
- Updates existing repositories
|
- Updates existing repositories
|
||||||
|
|
||||||
@@ -95,6 +99,7 @@ The `cheat` command-line tool is organized into several key packages:
|
|||||||
- **Override mechanism**: Local sheets override community sheets with same name
|
- **Override mechanism**: Local sheets override community sheets with same name
|
||||||
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
||||||
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
||||||
|
- **Directory-scoped discovery**: Walks up from cwd to find the nearest `.cheat` directory (like `.git` discovery)
|
||||||
|
|
||||||
### Sheet Format
|
### Sheet Format
|
||||||
|
|
||||||
@@ -114,4 +119,4 @@ ssh -L 8080:localhost:80 user@remote
|
|||||||
- Use `go-git` for repository operations, not exec'ing git commands
|
- Use `go-git` for repository operations, not exec'ing git commands
|
||||||
- Platform-specific paths are handled in `internal/config/paths.go`
|
- Platform-specific paths are handled in `internal/config/paths.go`
|
||||||
- Color output uses ANSI codes via the Chroma library
|
- Color output uses ANSI codes via the Chroma library
|
||||||
- Test files use the `internal/mock` package for test data
|
- Test files use the `mocks` package for test data
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
Contributing
|
# Contributing
|
||||||
============
|
|
||||||
|
|
||||||
Thank you for your interest in `cheat`.
|
Thank you for your interest in `cheat`.
|
||||||
|
|
||||||
@@ -11,4 +10,8 @@ Bug reports are still welcome. If you've found a bug, please open an issue in
|
|||||||
the [issue tracker][issues]. Before doing so, please search through the
|
the [issue tracker][issues]. Before doing so, please search through the
|
||||||
existing open issues to make sure it hasn't already been reported.
|
existing open issues to make sure it hasn't already been reported.
|
||||||
|
|
||||||
|
Feature requests may be filed, but are unlikely to be implemented. The project
|
||||||
|
is now mature and the maintainer considers its feature set to be essentially
|
||||||
|
complete.
|
||||||
|
|
||||||
[issues]: https://github.com/cheat/cheat/issues
|
[issues]: https://github.com/cheat/cheat/issues
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# NB: this image isn't used anywhere in the build pipeline. It exists to
|
# NB: this image isn't used anywhere in the build pipeline. It exists to
|
||||||
# conveniently facilitate ad-hoc experimentation in a sandboxed environment
|
# conveniently facilitate ad-hoc experimentation in a sandboxed environment
|
||||||
# during development.
|
# during development.
|
||||||
FROM golang:1.15-alpine
|
FROM golang:1.26-alpine
|
||||||
|
|
||||||
RUN apk add git less make
|
RUN apk add git less make
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ The main configuration structure:
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Colorize bool `yaml:"colorize"`
|
Colorize bool `yaml:"colorize"`
|
||||||
Editor string `yaml:"editor"`
|
Editor string `yaml:"editor"`
|
||||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
Cheatpaths []cp.Path `yaml:"cheatpaths"`
|
||||||
Style string `yaml:"style"`
|
Style string `yaml:"style"`
|
||||||
Formatter string `yaml:"formatter"`
|
Formatter string `yaml:"formatter"`
|
||||||
Pager string `yaml:"pager"`
|
Pager string `yaml:"pager"`
|
||||||
@@ -97,7 +97,7 @@ type Config struct {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Key functions:
|
Key functions:
|
||||||
- `New(opts, confPath, resolve)` - Load config from file
|
- `New(confPath, resolve)` - Load config from file
|
||||||
- `Validate()` - Validate configuration values
|
- `Validate()` - Validate configuration values
|
||||||
- `Editor()` - Get editor from environment or defaults (package-level function)
|
- `Editor()` - Get editor from environment or defaults (package-level function)
|
||||||
- `Pager()` - Get pager from environment or defaults (package-level function)
|
- `Pager()` - Get pager from environment or defaults (package-level function)
|
||||||
@@ -107,7 +107,7 @@ Key functions:
|
|||||||
Represents a directory containing cheatsheets:
|
Represents a directory containing cheatsheets:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Cheatpath struct {
|
type Path struct {
|
||||||
Name string // Friendly name (e.g., "personal")
|
Name string // Friendly name (e.g., "personal")
|
||||||
Path string // Filesystem path
|
Path string // Filesystem path
|
||||||
Tags []string // Tags applied to all sheets in this path
|
Tags []string // Tags applied to all sheets in this path
|
||||||
@@ -202,7 +202,7 @@ go test ./... # Go test directly
|
|||||||
Test files follow Go conventions:
|
Test files follow Go conventions:
|
||||||
- `*_test.go` files in same package
|
- `*_test.go` files in same package
|
||||||
- Table-driven tests for multiple scenarios
|
- Table-driven tests for multiple scenarios
|
||||||
- Mock data in `internal/mock` package
|
- Mock data in `mocks` package
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
Installing
|
# Installing
|
||||||
==========
|
|
||||||
`cheat` has no runtime dependencies. As such, installing it is generally
|
`cheat` has no runtime dependencies. As such, installing it is generally
|
||||||
straightforward. There are a few methods available:
|
straightforward. There are a few methods available:
|
||||||
|
|
||||||
### Install manually
|
## Install manually
|
||||||
#### Unix-like
|
### Unix-like
|
||||||
On Unix-like systems, you may simply paste the following snippet into your terminal:
|
On Unix-like systems, you may simply paste the following snippet into your terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /tmp \
|
cd /tmp \
|
||||||
&& wget https://github.com/cheat/cheat/releases/download/4.5.0/cheat-linux-amd64.gz \
|
&& wget https://github.com/cheat/cheat/releases/download/4.7.0/cheat-linux-amd64.gz \
|
||||||
&& gunzip cheat-linux-amd64.gz \
|
&& gunzip cheat-linux-amd64.gz \
|
||||||
&& chmod +x cheat-linux-amd64 \
|
&& chmod +x cheat-linux-amd64 \
|
||||||
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
||||||
```
|
```
|
||||||
|
|
||||||
You may need to need to change the version number (`4.5.0`) and the archive
|
You may need to need to change the version number (`4.7.0`) and the archive
|
||||||
(`cheat-linux-amd64.gz`) depending on your platform.
|
(`cheat-linux-amd64.gz`) depending on your platform.
|
||||||
|
|
||||||
See the [releases page][releases] for a list of supported platforms.
|
See the [releases page][releases] for a list of supported platforms.
|
||||||
|
|
||||||
#### Windows
|
### Windows
|
||||||
On Windows, download the appropriate binary from the [releases page][releases],
|
On Windows, download the appropriate binary from the [releases page][releases],
|
||||||
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
||||||
|
|
||||||
### Install via `go install`
|
## Install via `go install`
|
||||||
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
||||||
`cheat` via `go install`:
|
`cheat` via `go install`:
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ If you have `go` version `>=1.17` available on your `PATH`, you can install
|
|||||||
go install github.com/cheat/cheat/cmd/cheat@latest
|
go install github.com/cheat/cheat/cmd/cheat@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install via package manager
|
## Install via package manager
|
||||||
Several community-maintained packages are also available:
|
Several community-maintained packages are also available:
|
||||||
|
|
||||||
Package manager | Package(s)
|
Package manager | Package(s)
|
||||||
@@ -43,8 +42,6 @@ docker | [docker-cheat][pkg-docker]
|
|||||||
nix | [nixos.cheat][pkg-nix]
|
nix | [nixos.cheat][pkg-nix]
|
||||||
snap | [cheat][pkg-snap]
|
snap | [cheat][pkg-snap]
|
||||||
|
|
||||||
<!--[pacman][] |-->
|
|
||||||
|
|
||||||
## Configuring
|
## Configuring
|
||||||
Three things must be done before you can use `cheat`:
|
Three things must be done before you can use `cheat`:
|
||||||
1. A config file must be generated
|
1. A config file must be generated
|
||||||
@@ -56,7 +53,7 @@ automatically. After the installer is complete, it is strongly advised that you
|
|||||||
view the configuration file that was generated, as you may want to change some
|
view the configuration file that was generated, as you may want to change some
|
||||||
of its default values (to enable colorization, change the paginator, etc).
|
of its default values (to enable colorization, change the paginator, etc).
|
||||||
|
|
||||||
### conf.yml ###
|
### conf.yml
|
||||||
`cheat` is configured by a YAML file that will be auto-generated on first run.
|
`cheat` is configured by a YAML file that will be auto-generated on first run.
|
||||||
|
|
||||||
By default, the config file is assumed to exist on an XDG-compliant
|
By default, the config file is assumed to exist on an XDG-compliant
|
||||||
@@ -72,8 +69,8 @@ export CHEAT_CONFIG_PATH="~/.dotfiles/cheat/conf.yml"
|
|||||||
[community]: https://github.com/cheat/cheatsheets/
|
[community]: https://github.com/cheat/cheatsheets/
|
||||||
[pkg-aur-cheat-bin]: https://aur.archlinux.org/packages/cheat-bin
|
[pkg-aur-cheat-bin]: https://aur.archlinux.org/packages/cheat-bin
|
||||||
[pkg-aur-cheat]: https://aur.archlinux.org/packages/cheat
|
[pkg-aur-cheat]: https://aur.archlinux.org/packages/cheat
|
||||||
[pkg-brew]: https://formulae.brew.sh/formula/cheat
|
[pkg-brew]: https://formulae.brew.sh/formula/cheat
|
||||||
[pkg-docker]: https://github.com/bannmann/docker-cheat
|
[pkg-docker]: https://github.com/bannmann/docker-cheat
|
||||||
[pkg-nix]: https://search.nixos.org/packages?channel=unstable&show=cheat&from=0&size=50&sort=relevance&type=packages&query=cheat
|
[pkg-nix]: https://search.nixos.org/packages?channel=unstable&show=cheat&from=0&size=50&sort=relevance&type=packages&query=cheat
|
||||||
[pkg-snap]: https://snapcraft.io/cheat
|
[pkg-snap]: https://snapcraft.io/cheat
|
||||||
[releases]: https://github.com/cheat/cheat/releases
|
[releases]: https://github.com/cheat/cheat/releases
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -27,6 +27,7 @@ ZIP := zip -m
|
|||||||
docker_image := cheat-devel:latest
|
docker_image := cheat-devel:latest
|
||||||
|
|
||||||
# build flags
|
# build flags
|
||||||
|
export CGO_ENABLED := 0
|
||||||
BUILD_FLAGS := -ldflags="-s -w" -mod vendor -trimpath
|
BUILD_FLAGS := -ldflags="-s -w" -mod vendor -trimpath
|
||||||
GOBIN :=
|
GOBIN :=
|
||||||
TMPDIR := /tmp
|
TMPDIR := /tmp
|
||||||
@@ -43,6 +44,7 @@ releases := \
|
|||||||
$(dist_dir)/cheat-linux-arm7 \
|
$(dist_dir)/cheat-linux-arm7 \
|
||||||
$(dist_dir)/cheat-netbsd-amd64 \
|
$(dist_dir)/cheat-netbsd-amd64 \
|
||||||
$(dist_dir)/cheat-openbsd-amd64 \
|
$(dist_dir)/cheat-openbsd-amd64 \
|
||||||
|
$(dist_dir)/cheat-plan9-amd64 \
|
||||||
$(dist_dir)/cheat-solaris-amd64 \
|
$(dist_dir)/cheat-solaris-amd64 \
|
||||||
$(dist_dir)/cheat-windows-amd64.exe
|
$(dist_dir)/cheat-windows-amd64.exe
|
||||||
|
|
||||||
@@ -212,12 +214,12 @@ test-all: test test-integration
|
|||||||
## test-fuzz: run quick fuzz tests for security-critical functions
|
## test-fuzz: run quick fuzz tests for security-critical functions
|
||||||
.PHONY: test-fuzz
|
.PHONY: test-fuzz
|
||||||
test-fuzz:
|
test-fuzz:
|
||||||
@./build/fuzz.sh 15s
|
@./test/fuzz.sh 15s
|
||||||
|
|
||||||
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
||||||
.PHONY: test-fuzz-long
|
.PHONY: test-fuzz-long
|
||||||
test-fuzz-long:
|
test-fuzz-long:
|
||||||
@./build/fuzz.sh 10m
|
@./test/fuzz.sh 10m
|
||||||
|
|
||||||
## coverage: generate a test coverage report
|
## coverage: generate a test coverage report
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
@@ -239,22 +241,22 @@ coverage-text: .tmp
|
|||||||
## benchmark: run performance benchmarks
|
## benchmark: run performance benchmarks
|
||||||
.PHONY: benchmark
|
.PHONY: benchmark
|
||||||
benchmark: .tmp
|
benchmark: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./test/integration | tee .tmp/benchmark-latest.txt && \
|
||||||
$(RM) -f cheat.test
|
$(RM) -f integration.test
|
||||||
|
|
||||||
## benchmark-cpu: run benchmarks with CPU profiling
|
## benchmark-cpu: run benchmarks with CPU profiling
|
||||||
.PHONY: benchmark-cpu
|
.PHONY: benchmark-cpu
|
||||||
benchmark-cpu: .tmp
|
benchmark-cpu: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./test/integration && \
|
||||||
$(RM) -f cheat.test && \
|
$(RM) -f integration.test && \
|
||||||
echo "CPU profile saved to .tmp/cpu.prof" && \
|
echo "CPU profile saved to .tmp/cpu.prof" && \
|
||||||
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
||||||
|
|
||||||
## benchmark-mem: run benchmarks with memory profiling
|
## benchmark-mem: run benchmarks with memory profiling
|
||||||
.PHONY: benchmark-mem
|
.PHONY: benchmark-mem
|
||||||
benchmark-mem: .tmp
|
benchmark-mem: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./test/integration && \
|
||||||
$(RM) -f cheat.test && \
|
$(RM) -f integration.test && \
|
||||||
echo "Memory profile saved to .tmp/mem.prof" && \
|
echo "Memory profile saved to .tmp/mem.prof" && \
|
||||||
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
||||||
|
|
||||||
|
|||||||
54
README.md
54
README.md
@@ -1,8 +1,6 @@
|
|||||||

|

|
||||||
|
|
||||||
|
# cheat
|
||||||
cheat
|
|
||||||
=====
|
|
||||||
|
|
||||||
`cheat` allows you to create and view interactive cheatsheets on the
|
`cheat` allows you to create and view interactive cheatsheets on the
|
||||||
command-line. It was designed to help remind \*nix system administrators of
|
command-line. It was designed to help remind \*nix system administrators of
|
||||||
@@ -13,9 +11,7 @@ remember.
|
|||||||
|
|
||||||
Use `cheat` with [cheatsheets][].
|
Use `cheat` with [cheatsheets][].
|
||||||
|
|
||||||
|
## Example
|
||||||
Example
|
|
||||||
-------
|
|
||||||
The next time you're forced to disarm a nuclear weapon without consulting
|
The next time you're forced to disarm a nuclear weapon without consulting
|
||||||
Google, you may run:
|
Google, you may run:
|
||||||
|
|
||||||
@@ -42,8 +38,10 @@ tar -xjvf '/path/to/foo.tgz'
|
|||||||
tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
|
tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
|
||||||
```
|
```
|
||||||
|
|
||||||
Usage
|
## Installing
|
||||||
-----
|
For installation and configuration instructions, see [INSTALLING.md][].
|
||||||
|
|
||||||
|
## Usage
|
||||||
To view a cheatsheet:
|
To view a cheatsheet:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -70,6 +68,12 @@ To list all available cheatsheets:
|
|||||||
cheat -l
|
cheat -l
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To briefly list all cheatsheets (names and tags only):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cheat -b
|
||||||
|
```
|
||||||
|
|
||||||
To list all cheatsheets that are tagged with "networking":
|
To list all cheatsheets that are tagged with "networking":
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -101,14 +105,7 @@ Flags may be combined in intuitive ways. Example: to search sheets on the
|
|||||||
cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Cheatsheets
|
||||||
|
|
||||||
Installing
|
|
||||||
----------
|
|
||||||
For installation and configuration instructions, see [INSTALLING.md][].
|
|
||||||
|
|
||||||
Cheatsheets
|
|
||||||
-----------
|
|
||||||
Cheatsheets are plain-text files with no file extension, and are named
|
Cheatsheets are plain-text files with no file extension, and are named
|
||||||
according to the command used to view them:
|
according to the command used to view them:
|
||||||
|
|
||||||
@@ -129,12 +126,15 @@ tags: [ array, map ]
|
|||||||
const squares = [1, 2, 3, 4].map(x => x * x);
|
const squares = [1, 2, 3, 4].map(x => x * x);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Syntax highlighting is provided by [Chroma][], and the `syntax` value may be
|
||||||
|
set to any lexer name that Chroma supports. See Chroma's [supported
|
||||||
|
languages][] for a complete list.
|
||||||
|
|
||||||
The `cheat` executable includes no cheatsheets, but [community-sourced
|
The `cheat` executable includes no cheatsheets, but [community-sourced
|
||||||
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
||||||
install the community-sourced cheatsheets the first time you run `cheat`.
|
install the community-sourced cheatsheets the first time you run `cheat`.
|
||||||
|
|
||||||
Cheatpaths
|
## Cheatpaths
|
||||||
----------
|
|
||||||
Cheatsheets are stored on "cheatpaths", which are directories that contain
|
Cheatsheets are stored on "cheatpaths", which are directories that contain
|
||||||
cheatsheets. Cheatpaths are specified in the `conf.yml` file.
|
cheatsheets. Cheatpaths are specified in the `conf.yml` file.
|
||||||
|
|
||||||
@@ -166,14 +166,15 @@ If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
|
|||||||
transparently copy that sheet to a writeable directory before opening it for
|
transparently copy that sheet to a writeable directory before opening it for
|
||||||
editing.
|
editing.
|
||||||
|
|
||||||
### Directory-scoped Cheatpaths ###
|
### Directory-scoped Cheatpaths
|
||||||
At times, it can be useful to closely associate cheatsheets with a directory on
|
At times, it can be useful to closely associate cheatsheets with a directory on
|
||||||
your filesystem. `cheat` facilitates this by searching for a `.cheat` folder in
|
your filesystem. `cheat` facilitates this by searching for a `.cheat` directory
|
||||||
the current working directory. If found, the `.cheat` directory will
|
in the current working directory and its ancestors (similar to how `git` locates
|
||||||
(temporarily) be added to the cheatpaths.
|
`.git` directories). The nearest `.cheat` directory found will (temporarily) be
|
||||||
|
added to the cheatpaths. This means you can place a `.cheat` directory at your
|
||||||
|
project root and it will be available from any subdirectory within that project.
|
||||||
|
|
||||||
Autocompletion
|
## Autocompletion
|
||||||
--------------
|
|
||||||
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
|
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
|
||||||
the relevant [completion script][completions] into the appropriate directory on
|
the relevant [completion script][completions] into the appropriate directory on
|
||||||
your filesystem to enable autocompletion. (This directory will vary depending
|
your filesystem to enable autocompletion. (This directory will vary depending
|
||||||
@@ -189,5 +190,6 @@ Additionally, `cheat` supports enhanced autocompletion via integration with
|
|||||||
[Releases]: https://github.com/cheat/cheat/releases
|
[Releases]: https://github.com/cheat/cheat/releases
|
||||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||||
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||||
[fzf]: https://github.com/junegunn/fzf
|
[Chroma]: https://github.com/alecthomas/chroma
|
||||||
[go]: https://golang.org
|
[supported languages]: https://github.com/alecthomas/chroma#supported-languages
|
||||||
|
[fzf]: https://github.com/junegunn/fzf
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ The validation is performed at the application layer before any file operations
|
|||||||
|
|
||||||
### Validation Function
|
### Validation Function
|
||||||
|
|
||||||
The validation is implemented in `internal/cheatpath/validate.go`:
|
The validation is implemented in `internal/sheet/validate.go`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ValidateSheetName(name string) error {
|
func Validate(name string) error {
|
||||||
// Reject empty names
|
// Reject empty names
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||||
@@ -133,7 +133,7 @@ The following patterns are explicitly allowed:
|
|||||||
|
|
||||||
Comprehensive tests ensure the validation works correctly:
|
Comprehensive tests ensure the validation works correctly:
|
||||||
|
|
||||||
1. **Unit tests** (`internal/cheatpath/validate_test.go`) verify the validation logic
|
1. **Unit tests** (`internal/sheet/validate_test.go`) verify the validation logic
|
||||||
2. **Integration tests** verify the actual binary blocks malicious inputs
|
2. **Integration tests** verify the actual binary blocks malicious inputs
|
||||||
3. **No system files are accessed** during testing - all tests use isolated directories
|
3. **No system files are accessed** during testing - all tests use isolated directories
|
||||||
|
|
||||||
80
adr/004-recursive-cheat-directory-search.md
Normal file
80
adr/004-recursive-cheat-directory-search.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# ADR-004: Recursive `.cheat` Directory Search
|
||||||
|
|
||||||
|
Date: 2026-02-15
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Previously, `cheat` only checked the current working directory for a `.cheat`
|
||||||
|
subdirectory to use as a directory-scoped cheatpath. If a user was in
|
||||||
|
`~/projects/myapp/src/handlers/` but the `.cheat` directory lived at
|
||||||
|
`~/projects/myapp/.cheat`, it would not be found. Users requested (#602) that
|
||||||
|
`cheat` walk up the directory hierarchy to find the nearest `.cheat`
|
||||||
|
directory, mirroring the discovery pattern used by `git` for `.git`
|
||||||
|
directories.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Walk upward from the current working directory to the filesystem root, and
|
||||||
|
stop at the first `.cheat` directory found. Only directories are matched (a
|
||||||
|
file named `.cheat` is ignored).
|
||||||
|
|
||||||
|
### Stop at first `.cheat` found
|
||||||
|
|
||||||
|
Rather than collecting multiple `.cheat` directories from ancestor directories:
|
||||||
|
|
||||||
|
- Matches `.git` discovery semantics, which users already understand
|
||||||
|
- Fits the existing single-cheatpath-named-`"cwd"` code without structural
|
||||||
|
changes
|
||||||
|
- Avoids precedence and naming complexity when multiple `.cheat` directories
|
||||||
|
exist in the ancestor chain
|
||||||
|
- `cheat` already supports multiple cheatpaths via `conf.yml` for users who
|
||||||
|
want that; directory-scoped `.cheat` serves the project-context use case
|
||||||
|
|
||||||
|
### Walk to filesystem root (not `$HOME`)
|
||||||
|
|
||||||
|
Rather than stopping the search at `$HOME`:
|
||||||
|
|
||||||
|
- Simpler implementation with no platform-specific home-directory detection
|
||||||
|
- Supports sysadmins working in `/etc`, `/srv`, `/var`, or other paths
|
||||||
|
outside `$HOME`
|
||||||
|
- The boundary only matters on the failure path (no `.cheat` found anywhere),
|
||||||
|
where the cost is a few extra `stat` calls
|
||||||
|
- Security is not a concern since cheatsheets are display-only text, not
|
||||||
|
executable code
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Users can place `.cheat` at their project root and it works from any
|
||||||
|
subdirectory, matching their mental model
|
||||||
|
- No configuration changes needed; existing `.cheat` directories continue to
|
||||||
|
work identically
|
||||||
|
- Minimal code change (one small helper function)
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- A `.cheat` directory in an unexpected ancestor could be picked up
|
||||||
|
unintentionally, though this is unlikely in practice and matches how `.git`
|
||||||
|
works
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- The cheatpath name remains `"cwd"` regardless of which ancestor the `.cheat`
|
||||||
|
was found in
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### 1. Stop at `$HOME`
|
||||||
|
**Rejected**: Adds platform-specific complexity for minimal benefit. The only
|
||||||
|
downside of walking to root is a few extra `stat` calls on the failure path.
|
||||||
|
|
||||||
|
### 2. Collect multiple `.cheat` directories
|
||||||
|
**Rejected**: Introduces precedence and naming complexity. Users who want
|
||||||
|
multiple cheatpaths can configure them in `conf.yml`.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- GitHub issue: #602
|
||||||
|
- Implementation: `findLocalCheatpath()` in `internal/config/config.go`
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
cheatsheet := opts["--edit"].(string)
|
cheatsheet := opts["--edit"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
if err := sheet.Validate(cheatsheet); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -29,8 +30,6 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -52,55 +51,36 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
// if the sheet exists and is not read-only, edit it in place
|
// if the sheet exists and is not read-only, edit it in place
|
||||||
if ok && !sheet.ReadOnly {
|
if ok && !sheet.ReadOnly {
|
||||||
editpath = sheet.Path
|
editpath = sheet.Path
|
||||||
|
} else {
|
||||||
// if the sheet exists but is read-only, copy it before editing
|
// for read-only or non-existent sheets, resolve a writeable path
|
||||||
} else if ok && sheet.ReadOnly {
|
|
||||||
// compute the new edit path
|
|
||||||
// begin by getting a writeable cheatpath
|
|
||||||
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute the new edit path
|
// use the existing title for read-only copies, the requested name otherwise
|
||||||
editpath = filepath.Join(writepath.Path, sheet.Title)
|
title := cheatsheet
|
||||||
|
if ok {
|
||||||
|
title = sheet.Title
|
||||||
|
}
|
||||||
|
editpath = filepath.Join(writepath.Path, title)
|
||||||
|
|
||||||
// create any necessary subdirectories
|
if ok {
|
||||||
dirs := filepath.Dir(editpath)
|
// copy the read-only sheet to the writeable path
|
||||||
if dirs != "." {
|
// (Copy handles MkdirAll internally)
|
||||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
if err := sheet.Copy(editpath); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
fmt.Fprintf(os.Stderr, "failed to copy cheatsheet: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// create any necessary subdirectories for the new sheet
|
||||||
// copy the sheet to the new edit path
|
dirs := filepath.Dir(editpath)
|
||||||
err = sheet.Copy(editpath)
|
if dirs != "." {
|
||||||
if err != nil {
|
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to copy cheatsheet: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the sheet does not exist, create it
|
|
||||||
} else {
|
|
||||||
// compute the new edit path
|
|
||||||
// begin by getting a writeable cheatpath
|
|
||||||
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute the new edit path
|
|
||||||
editpath = filepath.Join(writepath.Path, cheatsheet)
|
|
||||||
|
|
||||||
// create any necessary subdirectories
|
|
||||||
dirs := filepath.Dir(editpath)
|
|
||||||
if dirs != "." {
|
|
||||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,54 +3,27 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdInit displays an example config file.
|
// cmdInit displays an example config file.
|
||||||
func cmdInit() {
|
func cmdInit(home string, envvars map[string]string) {
|
||||||
|
|
||||||
// get the user's home directory
|
// identify the os-specific paths at which configs may be located
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to get user home directory: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the envvars into a map of strings
|
|
||||||
envvars := map[string]string{}
|
|
||||||
for _, e := range os.Environ() {
|
|
||||||
pair := strings.SplitN(e, "=", 2)
|
|
||||||
envvars[pair[0]] = pair[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the config template
|
|
||||||
configs := configs()
|
|
||||||
|
|
||||||
// identify the os-specifc paths at which configs may be located
|
|
||||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to read config paths: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to read config paths: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine the appropriate paths for config data and (optional) community
|
|
||||||
// cheatsheets based on the user's platform
|
|
||||||
confpath := confpaths[0]
|
confpath := confpaths[0]
|
||||||
confdir := filepath.Dir(confpath)
|
|
||||||
|
|
||||||
// create paths for community and personal cheatsheets
|
// expand template placeholders and comment out community cheatpath
|
||||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
configs := installer.ExpandTemplate(configs(), confpath)
|
||||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
configs = installer.CommentCommunity(configs, confpath)
|
||||||
|
|
||||||
// template the above paths into the default configs
|
|
||||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
|
||||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
|
|
||||||
|
|
||||||
// output the templated configs
|
// output the templated configs
|
||||||
fmt.Println(configs)
|
fmt.Println(configs)
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatsheets by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -87,12 +85,17 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
|||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
||||||
|
|
||||||
// write a header row
|
|
||||||
fmt.Fprintln(w, "title:\tfile:\ttags:")
|
|
||||||
|
|
||||||
// generate sorted, columnized output
|
// generate sorted, columnized output
|
||||||
for _, sheet := range flattened {
|
if opts["--brief"].(bool) {
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ","))
|
fmt.Fprintln(w, "title:\ttags:")
|
||||||
|
for _, sheet := range flattened {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\n", sheet.Title, strings.Join(sheet.Tags, ","))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(w, "title:\tfile:\ttags:")
|
||||||
|
for _, sheet := range flattened {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ","))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write columnized output to stdout
|
// write columnized output to stdout
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
|||||||
cheatsheet := opts["--rm"].(string)
|
cheatsheet := opts["--rm"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
if err := sheet.Validate(cheatsheet); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -27,8 +27,6 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -80,7 +78,7 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
|||||||
// append the cheatsheet title
|
// append the cheatsheet title
|
||||||
sheet.Title,
|
sheet.Title,
|
||||||
// append the cheatsheet path
|
// append the cheatsheet path
|
||||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf.Color(opts)),
|
||||||
// indent each line of content
|
// indent each line of content
|
||||||
display.Indent(sheet.Text),
|
display.Indent(sheet.Text),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -42,7 +40,7 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
|||||||
// identify the matching cheatsheet
|
// identify the matching cheatsheet
|
||||||
out += fmt.Sprintf("%s %s\n",
|
out += fmt.Sprintf("%s %s\n",
|
||||||
sheet.Title,
|
sheet.Title,
|
||||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf.Color(opts)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// apply colorization if requested
|
// apply colorization if requested
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
// configs returns the default configuration template
|
// configs returns the default configuration template
|
||||||
func configs() string {
|
func configs() string {
|
||||||
return `---
|
return `---
|
||||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
# The editor to use with 'cheat -e <sheet>'. Overridden by $VISUAL or $EDITOR.
|
||||||
editor: EDITOR_PATH
|
editor: EDITOR_PATH
|
||||||
|
|
||||||
# Should 'cheat' always colorize output?
|
# Should 'cheat' always colorize output?
|
||||||
@@ -56,7 +56,8 @@ cheatpaths:
|
|||||||
tags: [ work ]
|
tags: [ work ]
|
||||||
readonly: false
|
readonly: false
|
||||||
|
|
||||||
# Community cheatsheets are stored here by default:
|
# Community cheatsheets (https://github.com/cheat/cheatsheets):
|
||||||
|
# To install: git clone https://github.com/cheat/cheatsheets COMMUNITY_PATH
|
||||||
- name: community
|
- name: community
|
||||||
path: COMMUNITY_PATH
|
path: COMMUNITY_PATH
|
||||||
tags: [ community ]
|
tags: [ community ]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/cheat/cheat/internal/installer"
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "4.5.0"
|
const version = "4.7.1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
@@ -26,13 +26,6 @@ func main() {
|
|||||||
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if --init was passed, we don't want to attempt to load a config file.
|
|
||||||
// Instead, just execute cmd_init and exit
|
|
||||||
if opts["--init"] != nil && opts["--init"] == true {
|
|
||||||
cmdInit()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the user's home directory
|
// get the user's home directory
|
||||||
home, err := homedir.Dir()
|
home, err := homedir.Dir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +44,13 @@ func main() {
|
|||||||
envvars[pair[0]] = pair[1]
|
envvars[pair[0]] = pair[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if --init was passed, we don't want to attempt to load a config file.
|
||||||
|
// Instead, just execute cmd_init and exit
|
||||||
|
if opts["--init"] == true {
|
||||||
|
cmdInit(home, envvars)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// identify the os-specifc paths at which configs may be located
|
// identify the os-specifc paths at which configs may be located
|
||||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +92,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize the configs
|
// initialize the configs
|
||||||
conf, err := config.New(opts, confpath, true)
|
conf, err := config.New(confpath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -129,7 +129,7 @@ func main() {
|
|||||||
case opts["--edit"] != nil:
|
case opts["--edit"] != nil:
|
||||||
cmd = cmdEdit
|
cmd = cmdEdit
|
||||||
|
|
||||||
case opts["--list"].(bool):
|
case opts["--list"].(bool), opts["--brief"].(bool):
|
||||||
cmd = cmdList
|
cmd = cmdList
|
||||||
|
|
||||||
case opts["--tags"].(bool):
|
case opts["--tags"].(bool):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ func usage() string {
|
|||||||
Options:
|
Options:
|
||||||
--init Write a default config file to stdout
|
--init Write a default config file to stdout
|
||||||
-a --all Search among all cheatpaths
|
-a --all Search among all cheatpaths
|
||||||
|
-b --brief List cheatsheets without file paths
|
||||||
-c --colorize Colorize output
|
-c --colorize Colorize output
|
||||||
-d --directories List cheatsheet directories
|
-d --directories List cheatsheet directories
|
||||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||||
@@ -41,8 +42,8 @@ Examples:
|
|||||||
To list all available cheatsheets:
|
To list all available cheatsheets:
|
||||||
cheat -l
|
cheat -l
|
||||||
|
|
||||||
To list all cheatsheets whose titles match "apt":
|
To briefly list all cheatsheets whose titles match "apt":
|
||||||
cheat -l apt
|
cheat -b apt
|
||||||
|
|
||||||
To list all tags in use:
|
To list all tags in use:
|
||||||
cheat -T
|
cheat -T
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# Architecture Decision Records
|
|
||||||
|
|
||||||
This directory contains Architecture Decision Records (ADRs) for the cheat project.
|
|
||||||
|
|
||||||
## What is an ADR?
|
|
||||||
|
|
||||||
An Architecture Decision Record captures an important architectural decision made along with its context and consequences. ADRs help us:
|
|
||||||
|
|
||||||
- Document why decisions were made
|
|
||||||
- Understand the context and trade-offs
|
|
||||||
- Review decisions when requirements change
|
|
||||||
- Onboard new contributors
|
|
||||||
|
|
||||||
## ADR Format
|
|
||||||
|
|
||||||
Each ADR follows this template:
|
|
||||||
|
|
||||||
1. **Title**: ADR-NNN: Brief description
|
|
||||||
2. **Date**: When the decision was made
|
|
||||||
3. **Status**: Proposed, Accepted, Deprecated, Superseded
|
|
||||||
4. **Context**: What prompted this decision?
|
|
||||||
5. **Decision**: What did we decide to do?
|
|
||||||
6. **Consequences**: What are the positive, negative, and neutral outcomes?
|
|
||||||
|
|
||||||
## Index of ADRs
|
|
||||||
|
|
||||||
| ADR | Title | Status | Date |
|
|
||||||
|-----|-------|--------|------|
|
|
||||||
| [001](001-path-traversal-protection.md) | Path Traversal Protection for Cheatsheet Names | Accepted | 2025-01-21 |
|
|
||||||
| [002](002-environment-variable-parsing.md) | No Defensive Checks for Environment Variable Parsing | Accepted | 2025-01-21 |
|
|
||||||
| [003](003-search-parallelization.md) | No Parallelization for Search Operations | Accepted | 2025-01-22 |
|
|
||||||
|
|
||||||
## Creating a New ADR
|
|
||||||
|
|
||||||
1. Copy the template from an existing ADR
|
|
||||||
2. Use the next sequential number
|
|
||||||
3. Fill in all sections
|
|
||||||
4. Include the ADR alongside the commit implementing the decision
|
|
||||||
@@ -23,6 +23,9 @@ Display the config file path.
|
|||||||
\-a, \[en]all
|
\-a, \[en]all
|
||||||
Search among all cheatpaths.
|
Search among all cheatpaths.
|
||||||
.TP
|
.TP
|
||||||
|
\-b, \[en]brief
|
||||||
|
List cheatsheets without file paths.
|
||||||
|
.TP
|
||||||
\-c, \[en]colorize
|
\-c, \[en]colorize
|
||||||
Colorize output.
|
Colorize output.
|
||||||
.TP
|
.TP
|
||||||
@@ -72,8 +75,8 @@ cheat \-d
|
|||||||
To list all available cheatsheets:
|
To list all available cheatsheets:
|
||||||
cheat \-l
|
cheat \-l
|
||||||
.TP
|
.TP
|
||||||
To list all cheatsheets whose titles match `apt':
|
To briefly list all cheatsheets whose titles match `apt':
|
||||||
cheat \-l \f[I]apt\f[R]
|
cheat \-b \f[I]apt\f[R]
|
||||||
.TP
|
.TP
|
||||||
To list all tags in use:
|
To list all tags in use:
|
||||||
cheat \-T
|
cheat \-T
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ OPTIONS
|
|||||||
-a, --all
|
-a, --all
|
||||||
: Search among all cheatpaths.
|
: Search among all cheatpaths.
|
||||||
|
|
||||||
|
-b, --brief
|
||||||
|
: List cheatsheets without file paths.
|
||||||
|
|
||||||
-c, --colorize
|
-c, --colorize
|
||||||
: Colorize output.
|
: Colorize output.
|
||||||
|
|
||||||
@@ -81,8 +84,8 @@ To view all cheatsheet directories:
|
|||||||
To list all available cheatsheets:
|
To list all available cheatsheets:
|
||||||
: cheat -l
|
: cheat -l
|
||||||
|
|
||||||
To list all cheatsheets whose titles match 'apt':
|
To briefly list all cheatsheets whose titles match 'apt':
|
||||||
: cheat -l _apt_
|
: cheat -b _apt_
|
||||||
|
|
||||||
To list all tags in use:
|
To list all tags in use:
|
||||||
: cheat -T
|
: cheat -T
|
||||||
|
|||||||
39
go.mod
39
go.mod
@@ -1,38 +1,37 @@
|
|||||||
module github.com/cheat/cheat
|
module github.com/cheat/cheat
|
||||||
|
|
||||||
go 1.19
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.12.0
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/go-git/go-git/v5 v5.11.0
|
github.com/go-git/go-git/v5 v5.16.5
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.5.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
|
||||||
golang.org/x/tools v0.16.1 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
152
go.sum
152
go.sum
@@ -1,140 +1,118 @@
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
|
||||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
@@ -2,27 +2,10 @@
|
|||||||
// management.
|
// management.
|
||||||
package cheatpath
|
package cheatpath
|
||||||
|
|
||||||
import "fmt"
|
// Path encapsulates cheatsheet path information
|
||||||
|
type Path struct {
|
||||||
// Cheatpath encapsulates cheatsheet path information
|
|
||||||
type Cheatpath struct {
|
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
ReadOnly bool `yaml:"readonly"`
|
ReadOnly bool `yaml:"readonly"`
|
||||||
Tags []string `yaml:"tags"`
|
Tags []string `yaml:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate ensures that the Cheatpath is valid
|
|
||||||
func (c Cheatpath) Validate() error {
|
|
||||||
// Check that name is not empty
|
|
||||||
if c.Name == "" {
|
|
||||||
return fmt.Errorf("cheatpath name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that path is not empty
|
|
||||||
if c.Path == "" {
|
|
||||||
return fmt.Errorf("cheatpath path cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import (
|
|||||||
func TestCheatpathValidate(t *testing.T) {
|
func TestCheatpathValidate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cheatpath Cheatpath
|
cheatpath Path
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid cheatpath",
|
name: "valid cheatpath",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: "/home/user/.config/cheat/personal",
|
Path: "/home/user/.config/cheat/personal",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -24,7 +24,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty name",
|
name: "empty name",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "",
|
Name: "",
|
||||||
Path: "/home/user/.config/cheat/personal",
|
Path: "/home/user/.config/cheat/personal",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -35,7 +35,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty path",
|
name: "empty path",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: "",
|
Path: "",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -46,7 +46,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both empty",
|
name: "both empty",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "",
|
Name: "",
|
||||||
Path: "",
|
Path: "",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
@@ -57,7 +57,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "minimal valid",
|
name: "minimal valid",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "x",
|
Name: "x",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
},
|
},
|
||||||
@@ -65,7 +65,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with readonly and tags",
|
name: "with readonly and tags",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: "/usr/share/cheat",
|
Path: "/usr/share/cheat",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
@@ -88,26 +88,3 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheatpathStruct(t *testing.T) {
|
|
||||||
// Test that the struct fields work as expected
|
|
||||||
cp := Cheatpath{
|
|
||||||
Name: "test",
|
|
||||||
Path: "/test/path",
|
|
||||||
ReadOnly: true,
|
|
||||||
Tags: []string{"tag1", "tag2"},
|
|
||||||
}
|
|
||||||
|
|
||||||
if cp.Name != "test" {
|
|
||||||
t.Errorf("expected Name to be 'test', got %q", cp.Name)
|
|
||||||
}
|
|
||||||
if cp.Path != "/test/path" {
|
|
||||||
t.Errorf("expected Path to be '/test/path', got %q", cp.Path)
|
|
||||||
}
|
|
||||||
if !cp.ReadOnly {
|
|
||||||
t.Error("expected ReadOnly to be true")
|
|
||||||
}
|
|
||||||
if len(cp.Tags) != 2 || cp.Tags[0] != "tag1" || cp.Tags[1] != "tag2" {
|
|
||||||
t.Errorf("expected Tags to be [tag1 tag2], got %v", cp.Tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// Package cheatpath manages collections of cheat sheets organized in filesystem directories.
|
|
||||||
//
|
|
||||||
// A Cheatpath represents a directory containing cheat sheets, with associated
|
|
||||||
// metadata such as tags and read-only status. Multiple cheatpaths can be
|
|
||||||
// configured to organize sheets from different sources (personal, community, work, etc.).
|
|
||||||
//
|
|
||||||
// # Cheatpath Structure
|
|
||||||
//
|
|
||||||
// Each cheatpath has:
|
|
||||||
// - Name: A friendly identifier (e.g., "personal", "community")
|
|
||||||
// - Path: The filesystem path to the directory
|
|
||||||
// - Tags: Tags automatically applied to all sheets in this path
|
|
||||||
// - ReadOnly: Whether sheets in this path can be modified
|
|
||||||
//
|
|
||||||
// Example configuration:
|
|
||||||
//
|
|
||||||
// cheatpaths:
|
|
||||||
// - name: personal
|
|
||||||
// path: ~/cheat
|
|
||||||
// tags: []
|
|
||||||
// readonly: false
|
|
||||||
// - name: community
|
|
||||||
// path: ~/cheat/community
|
|
||||||
// tags: [community]
|
|
||||||
// readonly: true
|
|
||||||
//
|
|
||||||
// # Directory-Scoped Cheatpaths
|
|
||||||
//
|
|
||||||
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
|
||||||
// When running cheat from a directory containing a `.cheat` subdirectory,
|
|
||||||
// that directory is temporarily added to the available cheatpaths.
|
|
||||||
//
|
|
||||||
// # Precedence and Overrides
|
|
||||||
//
|
|
||||||
// When multiple cheatpaths contain a sheet with the same name, the sheet
|
|
||||||
// from the most "local" cheatpath takes precedence. This allows users to
|
|
||||||
// override community sheets with personal versions.
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - Filter: Filters cheatpaths by name
|
|
||||||
// - Validate: Ensures cheatpath configuration is valid
|
|
||||||
// - Writeable: Returns the first writeable cheatpath
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Filter cheatpaths to only "personal"
|
|
||||||
// filtered, err := cheatpath.Filter(paths, "personal")
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Find a writeable cheatpath
|
|
||||||
// writeable, err := cheatpath.Writeable(paths)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Validate cheatpath configuration
|
|
||||||
// if err := cheatpath.Validate(paths); err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
package cheatpath
|
|
||||||
@@ -5,15 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Filter filters all cheatpaths that are not named `name`
|
// Filter filters all cheatpaths that are not named `name`
|
||||||
func Filter(paths []Cheatpath, name string) ([]Cheatpath, error) {
|
func Filter(paths []Path, name string) ([]Path, error) {
|
||||||
|
|
||||||
// if a path of the given name exists, return it
|
// if a path of the given name exists, return it
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
if path.Name == name {
|
if path.Name == name {
|
||||||
return []Cheatpath{path}, nil
|
return []Path{path}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return []Cheatpath{}, fmt.Errorf("cheatpath does not exist: %s", name)
|
return []Path{}, fmt.Errorf("cheatpath does not exist: %s", name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
func TestFilterSuccess(t *testing.T) {
|
func TestFilterSuccess(t *testing.T) {
|
||||||
|
|
||||||
// init cheatpaths
|
// init cheatpaths
|
||||||
paths := []Cheatpath{
|
paths := []Path{
|
||||||
Cheatpath{Name: "foo"},
|
Path{Name: "foo"},
|
||||||
Cheatpath{Name: "bar"},
|
Path{Name: "bar"},
|
||||||
Cheatpath{Name: "baz"},
|
Path{Name: "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the paths
|
// filter the paths
|
||||||
@@ -39,10 +39,10 @@ func TestFilterSuccess(t *testing.T) {
|
|||||||
func TestFilterFailure(t *testing.T) {
|
func TestFilterFailure(t *testing.T) {
|
||||||
|
|
||||||
// init cheatpaths
|
// init cheatpaths
|
||||||
paths := []Cheatpath{
|
paths := []Path{
|
||||||
Cheatpath{Name: "foo"},
|
Path{Name: "foo"},
|
||||||
Cheatpath{Name: "bar"},
|
Path{Name: "bar"},
|
||||||
Cheatpath{Name: "baz"},
|
Path{Name: "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the paths
|
// filter the paths
|
||||||
|
|||||||
@@ -2,39 +2,15 @@ package cheatpath
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateSheetName ensures that a cheatsheet name does not contain
|
// Validate ensures that the Path is valid
|
||||||
// directory traversal sequences or other potentially dangerous patterns.
|
func (c Path) Validate() error {
|
||||||
func ValidateSheetName(name string) error {
|
if c.Name == "" {
|
||||||
// Reject empty names
|
return fmt.Errorf("cheatpath name cannot be empty")
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
|
||||||
}
|
}
|
||||||
|
if c.Path == "" {
|
||||||
// Reject names containing directory traversal
|
return fmt.Errorf("cheatpath path cannot be empty")
|
||||||
if strings.Contains(name, "..") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject absolute paths
|
|
||||||
if filepath.IsAbs(name) {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot be an absolute path")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject names that start with ~ (home directory expansion)
|
|
||||||
if strings.HasPrefix(name, "~") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot start with '~'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject hidden files (files that start with a dot)
|
|
||||||
// We don't display hidden files, so we shouldn't create them
|
|
||||||
filename := filepath.Base(name)
|
|
||||||
if strings.HasPrefix(filename, ".") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Writeable returns a writeable Cheatpath
|
// Writeable returns a writeable Path
|
||||||
func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
|
func Writeable(cheatpaths []Path) (Path, error) {
|
||||||
|
|
||||||
// iterate backwards over the cheatpaths
|
// iterate backwards over the cheatpaths
|
||||||
// NB: we're going backwards because we assume that the most "local"
|
// NB: we're going backwards because we assume that the most "local"
|
||||||
@@ -18,5 +18,5 @@ func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return Cheatpath{}, fmt.Errorf("no writeable cheatpaths found")
|
return Path{}, fmt.Errorf("no writeable cheatpaths found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
func TestWriteableOK(t *testing.T) {
|
func TestWriteableOK(t *testing.T) {
|
||||||
|
|
||||||
// initialize some cheatpaths
|
// initialize some cheatpaths
|
||||||
cheatpaths := []Cheatpath{
|
cheatpaths := []Path{
|
||||||
Cheatpath{Path: "/foo", ReadOnly: true},
|
Path{Path: "/foo", ReadOnly: true},
|
||||||
Cheatpath{Path: "/bar", ReadOnly: false},
|
Path{Path: "/bar", ReadOnly: false},
|
||||||
Cheatpath{Path: "/baz", ReadOnly: true},
|
Path{Path: "/baz", ReadOnly: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the writeable cheatpath
|
// get the writeable cheatpath
|
||||||
@@ -34,10 +34,10 @@ func TestWriteableOK(t *testing.T) {
|
|||||||
func TestWriteableNotOK(t *testing.T) {
|
func TestWriteableNotOK(t *testing.T) {
|
||||||
|
|
||||||
// initialize some cheatpaths
|
// initialize some cheatpaths
|
||||||
cheatpaths := []Cheatpath{
|
cheatpaths := []Path{
|
||||||
Cheatpath{Path: "/foo", ReadOnly: true},
|
Path{Path: "/foo", ReadOnly: true},
|
||||||
Cheatpath{Path: "/bar", ReadOnly: true},
|
Path{Path: "/bar", ReadOnly: true},
|
||||||
Cheatpath{Path: "/baz", ReadOnly: true},
|
Path{Path: "/baz", ReadOnly: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the writeable cheatpath
|
// get the writeable cheatpath
|
||||||
|
|||||||
@@ -2,123 +2,16 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config encapsulates configuration parameters
|
// Config encapsulates configuration parameters
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Colorize bool `yaml:"colorize"`
|
Colorize bool `yaml:"colorize"`
|
||||||
Editor string `yaml:"editor"`
|
Editor string `yaml:"editor"`
|
||||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
Cheatpaths []cp.Path `yaml:"cheatpaths"`
|
||||||
Style string `yaml:"style"`
|
Style string `yaml:"style"`
|
||||||
Formatter string `yaml:"formatter"`
|
Formatter string `yaml:"formatter"`
|
||||||
Pager string `yaml:"pager"`
|
Pager string `yaml:"pager"`
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Config struct
|
|
||||||
func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error) {
|
|
||||||
|
|
||||||
// read the config file
|
|
||||||
buf, err := os.ReadFile(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize a config object
|
|
||||||
conf := Config{}
|
|
||||||
|
|
||||||
// store the config path
|
|
||||||
conf.Path = confPath
|
|
||||||
|
|
||||||
// unmarshal the yaml
|
|
||||||
err = yaml.Unmarshal(buf, &conf)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a .cheat directory exists locally, append it to the cheatpaths
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
local := filepath.Join(cwd, ".cheat")
|
|
||||||
if _, err := os.Stat(local); err == nil {
|
|
||||||
path := cp.Cheatpath{
|
|
||||||
Name: "cwd",
|
|
||||||
Path: local,
|
|
||||||
ReadOnly: false,
|
|
||||||
Tags: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// process cheatpaths
|
|
||||||
for i, cheatpath := range conf.Cheatpaths {
|
|
||||||
|
|
||||||
// expand ~ in config paths
|
|
||||||
expanded, err := homedir.Expand(cheatpath.Path)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("failed to expand ~: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow symlinks
|
|
||||||
//
|
|
||||||
// NB: `resolve` is an ugly kludge that exists for the sake of unit-tests.
|
|
||||||
// It's necessary because `EvalSymlinks` will error if the symlink points
|
|
||||||
// to a non-existent location on the filesystem. When unit-testing,
|
|
||||||
// however, we don't want to have dependencies on the filesystem. As such,
|
|
||||||
// `resolve` is a switch that allows us to turn off symlink resolution when
|
|
||||||
// running the config tests.
|
|
||||||
if resolve {
|
|
||||||
evaled, err := filepath.EvalSymlinks(expanded)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf(
|
|
||||||
"failed to resolve symlink: %s: %v",
|
|
||||||
expanded,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded = evaled
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Cheatpaths[i].Path = expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
// trim editor whitespace
|
|
||||||
conf.Editor = strings.TrimSpace(conf.Editor)
|
|
||||||
|
|
||||||
// if an editor was not provided in the configs, attempt to choose one
|
|
||||||
// that's appropriate for the environment
|
|
||||||
if conf.Editor == "" {
|
|
||||||
if conf.Editor, err = Editor(); err != nil {
|
|
||||||
return Config{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a chroma style was not provided, set a default
|
|
||||||
if conf.Style == "" {
|
|
||||||
conf.Style = "bw"
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a chroma formatter was not provided, set a default
|
|
||||||
if conf.Formatter == "" {
|
|
||||||
conf.Formatter = "terminal"
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the pager
|
|
||||||
conf.Pager = strings.TrimSpace(conf.Pager)
|
|
||||||
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestConfigYAMLErrors tests YAML parsing errors
|
// TestConfigYAMLErrors tests YAML parsing errors
|
||||||
@@ -18,71 +18,22 @@ func TestConfigYAMLErrors(t *testing.T) {
|
|||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
invalidYAML := filepath.Join(tempDir, "invalid.yml")
|
invalidYAML := filepath.Join(tempDir, "invalid.yml")
|
||||||
err = os.WriteFile(invalidYAML, []byte("invalid: yaml: content:\n - no closing"), 0644)
|
err = os.WriteFile(invalidYAML, []byte("cheatpaths: [{unclosed\n"), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to write invalid yaml: %v", err)
|
t.Fatalf("failed to write invalid yaml: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to load invalid YAML
|
// Attempt to load invalid YAML
|
||||||
_, err = New(map[string]interface{}{}, invalidYAML, false)
|
_, err = New(invalidYAML, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for invalid YAML, got nil")
|
t.Error("expected error for invalid YAML, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConfigLocalCheatpath tests local .cheat directory detection
|
|
||||||
func TestConfigLocalCheatpath(t *testing.T) {
|
|
||||||
// Create a temporary directory to act as working directory
|
|
||||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Save current working directory
|
|
||||||
oldCwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get cwd: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Chdir(oldCwd)
|
|
||||||
|
|
||||||
// Change to temp directory
|
|
||||||
err = os.Chdir(tempDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to change dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create .cheat directory
|
|
||||||
localCheat := filepath.Join(tempDir, ".cheat")
|
|
||||||
err = os.Mkdir(localCheat, 0755)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config
|
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to load config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that local cheatpath was added
|
|
||||||
found := false
|
|
||||||
for _, cp := range conf.Cheatpaths {
|
|
||||||
if cp.Name == "cwd" && cp.Path == localCheat {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
t.Error("local .cheat directory was not added to cheatpaths")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigDefaults tests default values
|
// TestConfigDefaults tests default values
|
||||||
func TestConfigDefaults(t *testing.T) {
|
func TestConfigDefaults(t *testing.T) {
|
||||||
// Load empty config
|
// Load empty config
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err := New(mocks.Path("conf/empty.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to load config: %v", err)
|
t.Errorf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -106,6 +57,12 @@ func TestConfigSymlinkResolution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Resolve symlinks in temp dir path (macOS /var -> /private/var)
|
||||||
|
tempDir, err = filepath.EvalSymlinks(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to resolve temp dir symlinks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create target directory
|
// Create target directory
|
||||||
targetDir := filepath.Join(tempDir, "target")
|
targetDir := filepath.Join(tempDir, "target")
|
||||||
err = os.Mkdir(targetDir, 0755)
|
err = os.Mkdir(targetDir, 0755)
|
||||||
@@ -135,13 +92,16 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load config with symlink resolution
|
// Load config with symlink resolution
|
||||||
conf, err := New(map[string]interface{}{}, configFile, true)
|
conf, err := New(configFile, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to load config: %v", err)
|
t.Errorf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify symlink was resolved
|
// Verify symlink was resolved
|
||||||
if len(conf.Cheatpaths) > 0 && conf.Cheatpaths[0].Path != targetDir {
|
if len(conf.Cheatpaths) == 0 {
|
||||||
|
t.Fatal("expected at least one cheatpath, got none")
|
||||||
|
}
|
||||||
|
if conf.Cheatpaths[0].Path != targetDir {
|
||||||
t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path)
|
t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,72 +136,13 @@ cheatpaths:
|
|||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config with symlink resolution should fail
|
// Load config with symlink resolution should skip the broken cheatpath
|
||||||
_, err = New(map[string]interface{}{}, configFile, true)
|
// (warn to stderr) rather than hard-error
|
||||||
if err == nil {
|
conf, err := New(configFile, true)
|
||||||
t.Error("expected error for broken symlink, got nil")
|
if err != nil {
|
||||||
|
t.Errorf("expected no error for broken symlink (should skip), got: %v", err)
|
||||||
|
}
|
||||||
|
if len(conf.Cheatpaths) != 0 {
|
||||||
|
t.Errorf("expected broken cheatpath to be filtered out, got %d cheatpaths", len(conf.Cheatpaths))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestConfigTildeExpansionError tests tilde expansion error handling
|
|
||||||
func TestConfigTildeExpansionError(t *testing.T) {
|
|
||||||
// This is tricky to test without mocking homedir.Expand
|
|
||||||
// We'll create a config with an invalid home reference
|
|
||||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
// Create config with user that likely doesn't exist
|
|
||||||
configContent := `---
|
|
||||||
editor: vim
|
|
||||||
cheatpaths:
|
|
||||||
- name: test
|
|
||||||
path: ~nonexistentuser12345/cheat
|
|
||||||
readonly: true
|
|
||||||
`
|
|
||||||
configFile := filepath.Join(tempDir, "config.yml")
|
|
||||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config - this may or may not fail depending on the system
|
|
||||||
// but we're testing that it doesn't panic
|
|
||||||
_, _ = New(map[string]interface{}{}, configFile, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigGetCwdError tests error handling when os.Getwd fails
|
|
||||||
func TestConfigGetCwdError(t *testing.T) {
|
|
||||||
// This is difficult to test without being able to break os.Getwd
|
|
||||||
// We'll create a scenario where the current directory is removed
|
|
||||||
|
|
||||||
// Create and enter a temp directory
|
|
||||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldCwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get cwd: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Chdir(oldCwd)
|
|
||||||
|
|
||||||
err = os.Chdir(tempDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to change dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the directory we're in
|
|
||||||
err = os.RemoveAll(tempDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to remove temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now os.Getwd should fail
|
|
||||||
_, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
|
||||||
// This might not fail on all systems, so we just ensure no panic
|
|
||||||
_ = err
|
|
||||||
}
|
|
||||||
|
|||||||
67
internal/config/config_fuzz_test.go
Normal file
67
internal/config/config_fuzz_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzFindLocalCheatpath exercises findLocalCheatpath with randomised
|
||||||
|
// directory depths and .cheat placements. For each fuzz input it builds a
|
||||||
|
// temporary directory hierarchy, places a single .cheat directory at a
|
||||||
|
// computed level, and asserts that the function always returns it.
|
||||||
|
func FuzzFindLocalCheatpath(f *testing.F) {
|
||||||
|
// Seed corpus: (totalDepth, cheatPlacement)
|
||||||
|
f.Add(uint8(1), uint8(0)) // depth 1, .cheat at root
|
||||||
|
f.Add(uint8(3), uint8(0)) // depth 3, .cheat at root
|
||||||
|
f.Add(uint8(5), uint8(3)) // depth 5, .cheat at level 3
|
||||||
|
f.Add(uint8(1), uint8(1)) // depth 1, .cheat at same level as search dir
|
||||||
|
f.Add(uint8(10), uint8(5)) // deep hierarchy
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, totalDepth uint8, cheatPlacement uint8) {
|
||||||
|
// Clamp to reasonable values to keep I/O bounded
|
||||||
|
depth := int(totalDepth%15) + 1 // 1..15
|
||||||
|
cheatAt := int(cheatPlacement) % (depth + 1) // 0..depth (0 = tempDir itself)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Build chain: tempDir/d0/d1/…/d{depth-1}
|
||||||
|
dirs := make([]string, 0, depth+1)
|
||||||
|
dirs = append(dirs, tempDir)
|
||||||
|
current := tempDir
|
||||||
|
for i := 0; i < depth; i++ {
|
||||||
|
current = filepath.Join(current, fmt.Sprintf("d%d", i))
|
||||||
|
if err := os.Mkdir(current, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
dirs = append(dirs, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place .cheat at dirs[cheatAt]
|
||||||
|
cheatDir := filepath.Join(dirs[cheatAt], ".cheat")
|
||||||
|
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir .cheat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search from the deepest directory
|
||||||
|
result := findLocalCheatpath(current)
|
||||||
|
|
||||||
|
// Invariant 1: must find the .cheat we placed
|
||||||
|
if result != cheatDir {
|
||||||
|
t.Errorf("depth=%d cheatAt=%d: expected %s, got %s",
|
||||||
|
depth, cheatAt, cheatDir, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 2: result must end with /.cheat
|
||||||
|
if !strings.HasSuffix(result, string(filepath.Separator)+".cheat") {
|
||||||
|
t.Errorf("result %q does not end with /.cheat", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 3: result must be under tempDir
|
||||||
|
if !strings.HasPrefix(result, tempDir) {
|
||||||
|
t.Errorf("result %q is not under tempDir %s", result, tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,20 +4,289 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathInCurrentDir tests that .cheat in the given dir is found
|
||||||
|
func TestFindLocalCheatpathInCurrentDir(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(tempDir)
|
||||||
|
if result != cheatDir {
|
||||||
|
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathInParent tests walking up to a parent directory
|
||||||
|
func TestFindLocalCheatpathInParent(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subDir := filepath.Join(tempDir, "sub")
|
||||||
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create sub dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(subDir)
|
||||||
|
if result != cheatDir {
|
||||||
|
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathInGrandparent tests walking up multiple levels
|
||||||
|
func TestFindLocalCheatpathInGrandparent(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deepDir := filepath.Join(tempDir, "a", "b", "c")
|
||||||
|
if err := os.MkdirAll(deepDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create deep dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(deepDir)
|
||||||
|
if result != cheatDir {
|
||||||
|
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathNearestWins tests that the closest .cheat is returned
|
||||||
|
func TestFindLocalCheatpathNearestWins(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create .cheat at root level
|
||||||
|
if err := os.Mkdir(filepath.Join(tempDir, ".cheat"), 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create root .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sub/.cheat (the nearer one)
|
||||||
|
subDir := filepath.Join(tempDir, "sub")
|
||||||
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create sub dir: %v", err)
|
||||||
|
}
|
||||||
|
nearCheatDir := filepath.Join(subDir, ".cheat")
|
||||||
|
if err := os.Mkdir(nearCheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create sub .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search from sub/deep/
|
||||||
|
deepDir := filepath.Join(subDir, "deep")
|
||||||
|
if err := os.Mkdir(deepDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create deep dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(deepDir)
|
||||||
|
if result != nearCheatDir {
|
||||||
|
t.Errorf("expected nearest %s, got %s", nearCheatDir, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathNotFound tests that empty string is returned when no .cheat exists
|
||||||
|
func TestFindLocalCheatpathNotFound(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
result := findLocalCheatpath(tempDir)
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("expected empty string, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathSkipsFile tests that a file named .cheat is not matched
|
||||||
|
func TestFindLocalCheatpathSkipsFile(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create .cheat as a file, not a directory
|
||||||
|
cheatFile := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.WriteFile(cheatFile, []byte("not a directory"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(tempDir)
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("expected empty string for .cheat file, got %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathSymlink tests that a .cheat symlink to a directory is found
|
||||||
|
func TestFindLocalCheatpathSymlink(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create the real directory
|
||||||
|
realDir := filepath.Join(tempDir, "real-cheat")
|
||||||
|
if err := os.Mkdir(realDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create real dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlink .cheat -> real-cheat
|
||||||
|
cheatLink := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.Symlink(realDir, cheatLink); err != nil {
|
||||||
|
t.Fatalf("failed to create symlink: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := findLocalCheatpath(tempDir)
|
||||||
|
if result != cheatLink {
|
||||||
|
t.Errorf("expected %s, got %s", cheatLink, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathSymlinkInAncestor tests discovery through a symlinked
|
||||||
|
// ancestor directory. When the cwd is reached via a symlink, filepath.Dir
|
||||||
|
// walks the symlinked path (not the real path), so .cheat must be findable
|
||||||
|
// through that chain.
|
||||||
|
func TestFindLocalCheatpathSymlinkInAncestor(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create real/project/.cheat
|
||||||
|
realProject := filepath.Join(tempDir, "real", "project")
|
||||||
|
if err := os.MkdirAll(realProject, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create real project dir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Mkdir(filepath.Join(realProject, ".cheat"), 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create symlink: linked -> real/project
|
||||||
|
linkedProject := filepath.Join(tempDir, "linked")
|
||||||
|
if err := os.Symlink(realProject, linkedProject); err != nil {
|
||||||
|
t.Fatalf("failed to create symlink: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sub inside the symlinked path
|
||||||
|
subDir := filepath.Join(linkedProject, "sub")
|
||||||
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create sub dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search from linked/sub — should find linked/.cheat
|
||||||
|
// (os.Stat follows symlinks, so linked/.cheat resolves to real/project/.cheat)
|
||||||
|
result := findLocalCheatpath(subDir)
|
||||||
|
expected := filepath.Join(linkedProject, ".cheat")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %s, got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindLocalCheatpathPermissionDenied tests that unreadable ancestor
|
||||||
|
// directories are skipped and the walk continues upward.
|
||||||
|
func TestFindLocalCheatpathPermissionDenied(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Unix permissions do not apply on Windows")
|
||||||
|
}
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
t.Skip("test requires non-root user")
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Resolve symlinks (macOS /var -> /private/var)
|
||||||
|
tempDir, err := filepath.EvalSymlinks(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to resolve symlinks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tempDir/.cheat (the target we want found)
|
||||||
|
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||||
|
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tempDir/restricted/ with its own .cheat and sub/
|
||||||
|
restricted := filepath.Join(tempDir, "restricted")
|
||||||
|
if err := os.Mkdir(restricted, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create restricted dir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Mkdir(filepath.Join(restricted, ".cheat"), 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create restricted .cheat dir: %v", err)
|
||||||
|
}
|
||||||
|
subDir := filepath.Join(restricted, "sub")
|
||||||
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create sub dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make restricted/ unreadable — blocks stat of children
|
||||||
|
if err := os.Chmod(restricted, 0000); err != nil {
|
||||||
|
t.Fatalf("failed to chmod: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.Chmod(restricted, 0755) })
|
||||||
|
|
||||||
|
// Walk from restricted/sub: stat("restricted/sub/.cheat") fails (EACCES),
|
||||||
|
// stat("restricted/.cheat") fails (EACCES), walk continues to tempDir/.cheat
|
||||||
|
result := findLocalCheatpath(subDir)
|
||||||
|
if result != cheatDir {
|
||||||
|
t.Errorf("expected %s (walked past restricted dir), got %s", cheatDir, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestConfig asserts that the configs are loaded correctly
|
// TestConfig asserts that the configs are loaded correctly
|
||||||
func TestConfigSuccessful(t *testing.T) {
|
func TestConfigSuccessful(t *testing.T) {
|
||||||
|
|
||||||
|
// Chdir into a temp directory so no ancestor .cheat directory can
|
||||||
|
// leak into the cheatpaths (findLocalCheatpath walks the full
|
||||||
|
// ancestor chain).
|
||||||
|
oldCwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get cwd: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Chdir(oldCwd)
|
||||||
|
if err := os.Chdir(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear env vars so they don't override the config file value
|
||||||
|
oldVisual := os.Getenv("VISUAL")
|
||||||
|
oldEditor := os.Getenv("EDITOR")
|
||||||
|
os.Unsetenv("VISUAL")
|
||||||
|
os.Unsetenv("EDITOR")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("VISUAL", oldVisual)
|
||||||
|
os.Setenv("EDITOR", oldEditor)
|
||||||
|
}()
|
||||||
|
|
||||||
// initialize a config
|
// initialize a config
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/conf.yml"), false)
|
conf, err := New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to parse config file: %v", err)
|
t.Errorf("failed to parse config file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -37,18 +306,18 @@ func TestConfigSuccessful(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assert that the cheatpaths are correct
|
// assert that the cheatpaths are correct
|
||||||
want := []cheatpath.Cheatpath{
|
want := []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "community"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "community"),
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
Tags: []string{"community"},
|
Tags: []string{"community"},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "work"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "work"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{"work"},
|
Tags: []string{"work"},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "personal"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "personal"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{"personal"},
|
Tags: []string{"personal"},
|
||||||
@@ -69,43 +338,84 @@ func TestConfigSuccessful(t *testing.T) {
|
|||||||
func TestConfigFailure(t *testing.T) {
|
func TestConfigFailure(t *testing.T) {
|
||||||
|
|
||||||
// attempt to read a non-existent config file
|
// attempt to read a non-existent config file
|
||||||
_, err := New(map[string]interface{}{}, "/does-not-exit", false)
|
_, err := New("/does-not-exit", false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("failed to error on unreadable config")
|
t.Errorf("failed to error on unreadable config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEmptyEditor asserts that envvars are respected if an editor is not
|
// TestEditorEnvOverride asserts that $VISUAL and $EDITOR override the
|
||||||
// specified in the configs
|
// config file value at runtime (regression test for #589)
|
||||||
func TestEmptyEditor(t *testing.T) {
|
func TestEditorEnvOverride(t *testing.T) {
|
||||||
|
// save and clear the environment variables
|
||||||
|
oldVisual := os.Getenv("VISUAL")
|
||||||
|
oldEditor := os.Getenv("EDITOR")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("VISUAL", oldVisual)
|
||||||
|
os.Setenv("EDITOR", oldEditor)
|
||||||
|
}()
|
||||||
|
|
||||||
// clear the environment variables
|
// with no env vars, the config file value should be used
|
||||||
os.Setenv("VISUAL", "")
|
os.Unsetenv("VISUAL")
|
||||||
os.Setenv("EDITOR", "")
|
os.Unsetenv("EDITOR")
|
||||||
|
conf, err := New(mocks.Path("conf/conf.yml"), false)
|
||||||
// initialize a config
|
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to initialize test: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
|
}
|
||||||
|
if conf.Editor != "vim" {
|
||||||
|
t.Errorf("expected config file editor: want: vim, got: %s", conf.Editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set editor, and assert that it is respected
|
// $EDITOR should override the config file value
|
||||||
os.Setenv("EDITOR", "foo")
|
os.Setenv("EDITOR", "nano")
|
||||||
conf, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err = New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
if conf.Editor != "foo" {
|
if conf.Editor != "nano" {
|
||||||
t.Errorf("failed to respect editor: want: foo, got: %s", conf.Editor)
|
t.Errorf("$EDITOR should override config: want: nano, got: %s", conf.Editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set visual, and assert that it overrides editor
|
// $VISUAL should override both $EDITOR and the config file value
|
||||||
os.Setenv("VISUAL", "bar")
|
os.Setenv("VISUAL", "emacs")
|
||||||
conf, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err = New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
if conf.Editor != "bar" {
|
if conf.Editor != "emacs" {
|
||||||
t.Errorf("failed to respect editor: want: bar, got: %s", conf.Editor)
|
t.Errorf("$VISUAL should override all: want: emacs, got: %s", conf.Editor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEditorEnvFallback asserts that env vars are used as fallback when
|
||||||
|
// no editor is specified in the config file
|
||||||
|
func TestEditorEnvFallback(t *testing.T) {
|
||||||
|
// save and clear the environment variables
|
||||||
|
oldVisual := os.Getenv("VISUAL")
|
||||||
|
oldEditor := os.Getenv("EDITOR")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("VISUAL", oldVisual)
|
||||||
|
os.Setenv("EDITOR", oldEditor)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// set $EDITOR and assert it's used when config has no editor
|
||||||
|
os.Unsetenv("VISUAL")
|
||||||
|
os.Setenv("EDITOR", "foo")
|
||||||
|
conf, err := New(mocks.Path("conf/empty.yml"), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
|
}
|
||||||
|
if conf.Editor != "foo" {
|
||||||
|
t.Errorf("failed to respect $EDITOR: want: foo, got: %s", conf.Editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set $VISUAL and assert it takes precedence over $EDITOR
|
||||||
|
os.Setenv("VISUAL", "bar")
|
||||||
|
conf, err = New(mocks.Path("conf/empty.yml"), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
|
}
|
||||||
|
if conf.Editor != "bar" {
|
||||||
|
t.Errorf("failed to respect $VISUAL: want: bar, got: %s", conf.Editor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
// Package config manages application configuration and settings.
|
|
||||||
//
|
|
||||||
// The config package provides functionality to:
|
|
||||||
// - Load configuration from YAML files
|
|
||||||
// - Validate configuration values
|
|
||||||
// - Manage platform-specific configuration paths
|
|
||||||
// - Handle editor and pager settings
|
|
||||||
// - Configure colorization and formatting options
|
|
||||||
//
|
|
||||||
// # Configuration Structure
|
|
||||||
//
|
|
||||||
// The main configuration file (conf.yml) contains:
|
|
||||||
// - Editor preferences
|
|
||||||
// - Pager settings
|
|
||||||
// - Colorization options
|
|
||||||
// - Cheatpath definitions
|
|
||||||
// - Formatting preferences
|
|
||||||
//
|
|
||||||
// Example configuration:
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// editor: vim
|
|
||||||
// colorize: true
|
|
||||||
// style: monokai
|
|
||||||
// formatter: terminal256
|
|
||||||
// pager: less -FRX
|
|
||||||
// cheatpaths:
|
|
||||||
// - name: personal
|
|
||||||
// path: ~/cheat
|
|
||||||
// tags: []
|
|
||||||
// readonly: false
|
|
||||||
// - name: community
|
|
||||||
// path: ~/cheat/.cheat
|
|
||||||
// tags: [community]
|
|
||||||
// readonly: true
|
|
||||||
//
|
|
||||||
// # Platform-Specific Paths
|
|
||||||
//
|
|
||||||
// The package automatically detects configuration paths based on the operating system:
|
|
||||||
// - Linux/Unix: $XDG_CONFIG_HOME/cheat/conf.yml or ~/.config/cheat/conf.yml
|
|
||||||
// - macOS: ~/Library/Application Support/cheat/conf.yml
|
|
||||||
// - Windows: %APPDATA%\cheat\conf.yml
|
|
||||||
//
|
|
||||||
// # Environment Variables
|
|
||||||
//
|
|
||||||
// The following environment variables are respected:
|
|
||||||
// - CHEAT_CONFIG_PATH: Override the configuration file location
|
|
||||||
// - CHEAT_USE_FZF: Enable fzf integration when set to "true"
|
|
||||||
// - EDITOR: Default editor if not specified in config
|
|
||||||
// - VISUAL: Fallback editor if EDITOR is not set
|
|
||||||
// - PAGER: Default pager if not specified in config
|
|
||||||
package config
|
|
||||||
@@ -3,7 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,18 +74,21 @@ func TestInitCreateDirectory(t *testing.T) {
|
|||||||
// TestInitWriteError tests error handling when file write fails
|
// TestInitWriteError tests error handling when file write fails
|
||||||
func TestInitWriteError(t *testing.T) {
|
func TestInitWriteError(t *testing.T) {
|
||||||
// Skip this test if running as root (can write anywhere)
|
// Skip this test if running as root (can write anywhere)
|
||||||
if os.Getuid() == 0 {
|
if runtime.GOOS != "windows" && os.Getuid() == 0 {
|
||||||
t.Skip("Cannot test write errors as root")
|
t.Skip("Cannot test write errors as root")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use a platform-appropriate invalid path
|
||||||
|
invalidPath := "/dev/null/impossible/path/conf.yml"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
invalidPath = `NUL\impossible\path\conf.yml`
|
||||||
|
}
|
||||||
|
|
||||||
// Try to write to a read-only directory
|
// Try to write to a read-only directory
|
||||||
err := Init("/dev/null/impossible/path/conf.yml", "test")
|
err := Init(invalidPath, "test")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error when writing to invalid path, got nil")
|
t.Error("expected error when writing to invalid path, got nil")
|
||||||
}
|
}
|
||||||
if err != nil && !strings.Contains(err.Error(), "failed to create") {
|
|
||||||
t.Errorf("expected 'failed to create' error, got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestInitExistingFile tests that Init overwrites existing files
|
// TestInitExistingFile tests that Init overwrites existing files
|
||||||
|
|||||||
147
internal/config/new.go
Normal file
147
internal/config/new.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new Config struct
|
||||||
|
func New(confPath string, resolve bool) (Config, error) {
|
||||||
|
|
||||||
|
// read the config file
|
||||||
|
buf, err := os.ReadFile(confPath)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize a config object
|
||||||
|
conf := Config{}
|
||||||
|
|
||||||
|
// store the config path
|
||||||
|
conf.Path = confPath
|
||||||
|
|
||||||
|
// unmarshal the yaml
|
||||||
|
err = yaml.Unmarshal(buf, &conf)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a .cheat directory exists in the current directory or any ancestor,
|
||||||
|
// append it to the cheatpaths
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if local := findLocalCheatpath(cwd); local != "" {
|
||||||
|
path := cp.Path{
|
||||||
|
Name: "cwd",
|
||||||
|
Path: local,
|
||||||
|
ReadOnly: false,
|
||||||
|
Tags: []string{},
|
||||||
|
}
|
||||||
|
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process cheatpaths
|
||||||
|
var validPaths []cp.Path
|
||||||
|
for _, cheatpath := range conf.Cheatpaths {
|
||||||
|
|
||||||
|
// expand ~ in config paths
|
||||||
|
expanded, err := homedir.Expand(cheatpath.Path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("failed to expand ~: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow symlinks
|
||||||
|
//
|
||||||
|
// NB: `resolve` is an ugly kludge that exists for the sake of unit-tests.
|
||||||
|
// It's necessary because `EvalSymlinks` will error if the symlink points
|
||||||
|
// to a non-existent location on the filesystem. When unit-testing,
|
||||||
|
// however, we don't want to have dependencies on the filesystem. As such,
|
||||||
|
// `resolve` is a switch that allows us to turn off symlink resolution when
|
||||||
|
// running the config tests.
|
||||||
|
if resolve {
|
||||||
|
evaled, err := filepath.EvalSymlinks(expanded)
|
||||||
|
if err != nil {
|
||||||
|
// if the path simply doesn't exist, warn and skip it
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"WARNING: cheatpath '%s' does not exist, skipping\n",
|
||||||
|
expanded,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return Config{}, fmt.Errorf(
|
||||||
|
"failed to resolve symlink: %s: %v",
|
||||||
|
expanded,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded = evaled
|
||||||
|
}
|
||||||
|
|
||||||
|
cheatpath.Path = expanded
|
||||||
|
validPaths = append(validPaths, cheatpath)
|
||||||
|
}
|
||||||
|
conf.Cheatpaths = validPaths
|
||||||
|
|
||||||
|
// determine the editor: env vars override the config file value,
|
||||||
|
// following standard Unix convention (see #589)
|
||||||
|
if v := os.Getenv("VISUAL"); v != "" {
|
||||||
|
conf.Editor = v
|
||||||
|
} else if v := os.Getenv("EDITOR"); v != "" {
|
||||||
|
conf.Editor = v
|
||||||
|
} else {
|
||||||
|
conf.Editor = strings.TrimSpace(conf.Editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an editor was still not determined, attempt to choose one
|
||||||
|
// that's appropriate for the environment
|
||||||
|
if conf.Editor == "" {
|
||||||
|
if conf.Editor, err = Editor(); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a chroma style was not provided, set a default
|
||||||
|
if conf.Style == "" {
|
||||||
|
conf.Style = "bw"
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a chroma formatter was not provided, set a default
|
||||||
|
if conf.Formatter == "" {
|
||||||
|
conf.Formatter = "terminal"
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the pager
|
||||||
|
conf.Pager = strings.TrimSpace(conf.Pager)
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findLocalCheatpath walks upward from dir looking for a .cheat directory.
|
||||||
|
// It returns the path to the first .cheat directory found, or an empty string
|
||||||
|
// if none exists. This mirrors the discovery pattern used by git for .git
|
||||||
|
// directories.
|
||||||
|
func findLocalCheatpath(dir string) string {
|
||||||
|
for {
|
||||||
|
candidate := filepath.Join(dir, ".cheat")
|
||||||
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewTrimsWhitespace(t *testing.T) {
|
func TestNewTrimsWhitespace(t *testing.T) {
|
||||||
|
// clear env vars so they don't override the config file value
|
||||||
|
oldVisual := os.Getenv("VISUAL")
|
||||||
|
oldEditor := os.Getenv("EDITOR")
|
||||||
|
os.Unsetenv("VISUAL")
|
||||||
|
os.Unsetenv("EDITOR")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("VISUAL", oldVisual)
|
||||||
|
os.Setenv("EDITOR", oldEditor)
|
||||||
|
}()
|
||||||
|
|
||||||
// Create a temporary config file with whitespace in editor and pager
|
// Create a temporary config file with whitespace in editor and pager
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configPath := filepath.Join(tmpDir, "config.yml")
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
@@ -28,7 +38,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -78,7 +88,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// It's OK if this fails due to no editor being found
|
// It's OK if this fails due to no editor being found
|
||||||
// The important thing is it doesn't panic
|
// The important thing is it doesn't panic
|
||||||
@@ -113,7 +123,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -44,29 +45,20 @@ func TestPager(t *testing.T) {
|
|||||||
os.Setenv("PAGER", "")
|
os.Setenv("PAGER", "")
|
||||||
pager := Pager()
|
pager := Pager()
|
||||||
|
|
||||||
// Should find one of the fallback pagers or return empty string
|
if pager == "" {
|
||||||
|
return // no pager found is acceptable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should find one of the known fallback pagers
|
||||||
validPagers := map[string]bool{
|
validPagers := map[string]bool{
|
||||||
"": true, // no pager found
|
|
||||||
"pager": true,
|
"pager": true,
|
||||||
"less": true,
|
"less": true,
|
||||||
"more": true,
|
"more": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a path to one of these
|
base := filepath.Base(pager)
|
||||||
found := false
|
if !validPagers[base] {
|
||||||
for p := range validPagers {
|
t.Errorf("unexpected pager value: %s (base: %s)", pager, base)
|
||||||
if p == "" && pager == "" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if p != "" && (pager == p || len(pager) >= len(p) && pager[len(pager)-len(p):] == p) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
t.Errorf("unexpected pager value: %s", pager)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -10,6 +12,9 @@ import (
|
|||||||
// TestValidatePathsNix asserts that the proper config paths are returned on
|
// TestValidatePathsNix asserts that the proper config paths are returned on
|
||||||
// *nix platforms
|
// *nix platforms
|
||||||
func TestValidatePathsNix(t *testing.T) {
|
func TestValidatePathsNix(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("filepath.Join uses backslashes on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
// mock the user's home directory
|
// mock the user's home directory
|
||||||
home := "/home/foo"
|
home := "/home/foo"
|
||||||
@@ -57,6 +62,9 @@ func TestValidatePathsNix(t *testing.T) {
|
|||||||
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned
|
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned
|
||||||
// on *nix platforms when `XDG_CONFIG_HOME is not set
|
// on *nix platforms when `XDG_CONFIG_HOME is not set
|
||||||
func TestValidatePathsNixNoXDG(t *testing.T) {
|
func TestValidatePathsNixNoXDG(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("filepath.Join uses backslashes on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
// mock the user's home directory
|
// mock the user's home directory
|
||||||
home := "/home/foo"
|
home := "/home/foo"
|
||||||
@@ -106,8 +114,8 @@ func TestValidatePathsWindows(t *testing.T) {
|
|||||||
|
|
||||||
// mock some envvars
|
// mock some envvars
|
||||||
envvars := map[string]string{
|
envvars := map[string]string{
|
||||||
"APPDATA": "/apps",
|
"APPDATA": filepath.Join("C:", "apps"),
|
||||||
"PROGRAMDATA": "/programs",
|
"PROGRAMDATA": filepath.Join("C:", "programs"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the paths for the platform
|
// get the paths for the platform
|
||||||
@@ -118,8 +126,8 @@ func TestValidatePathsWindows(t *testing.T) {
|
|||||||
|
|
||||||
// specify the expected output
|
// specify the expected output
|
||||||
want := []string{
|
want := []string{
|
||||||
"/apps/cheat/conf.yml",
|
filepath.Join("C:", "apps", "cheat", "conf.yml"),
|
||||||
"/programs/cheat/conf.yml",
|
filepath.Join("C:", "programs", "cheat", "conf.yml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// assert that output matches expectations
|
// assert that output matches expectations
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ func TestValidateCorrect(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -38,8 +38,8 @@ func TestInvalidateMissingEditor(t *testing.T) {
|
|||||||
conf := Config{
|
conf := Config{
|
||||||
Colorize: true,
|
Colorize: true,
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -71,19 +71,28 @@ func TestInvalidateMissingCheatpaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMissingInvalidFormatters asserts that configs which contain invalid
|
// TestInvalidateInvalidFormatter asserts that configs which contain invalid
|
||||||
// formatters are invalidated
|
// formatters are invalidated
|
||||||
func TestMissingInvalidFormatters(t *testing.T) {
|
func TestInvalidateInvalidFormatter(t *testing.T) {
|
||||||
|
|
||||||
// mock a config
|
// mock a config with a valid editor and cheatpaths but invalid formatter
|
||||||
conf := Config{
|
conf := Config{
|
||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
|
Formatter: "html",
|
||||||
|
Cheatpaths: []cheatpath.Path{
|
||||||
|
cheatpath.Path{
|
||||||
|
Name: "foo",
|
||||||
|
Path: "/foo",
|
||||||
|
ReadOnly: false,
|
||||||
|
Tags: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// assert that no errors are returned
|
// assert that the config is invalidated due to the formatter
|
||||||
if err := conf.Validate(); err == nil {
|
if err := conf.Validate(); err == nil {
|
||||||
t.Errorf("failed to invalidate config without formatter")
|
t.Errorf("failed to invalidate config with invalid formatter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +105,14 @@ func TestInvalidateDuplicateCheatpathNames(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/bar",
|
Path: "/bar",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -127,14 +136,14 @@ func TestInvalidateDuplicateCheatpathPaths(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "bar",
|
Name: "bar",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Package display handles output formatting and presentation for the cheat application.
|
|
||||||
//
|
|
||||||
// The display package provides utilities for:
|
|
||||||
// - Writing output to stdout or a pager
|
|
||||||
// - Formatting text with indentation
|
|
||||||
// - Creating faint (dimmed) text for de-emphasis
|
|
||||||
// - Managing colored output
|
|
||||||
//
|
|
||||||
// # Pager Integration
|
|
||||||
//
|
|
||||||
// The package integrates with system pagers (less, more, etc.) to handle
|
|
||||||
// long output. If a pager is configured and the output is to a terminal,
|
|
||||||
// content is automatically piped through the pager.
|
|
||||||
//
|
|
||||||
// # Text Formatting
|
|
||||||
//
|
|
||||||
// Various formatting utilities are provided:
|
|
||||||
// - Faint: Creates dimmed text using ANSI escape codes
|
|
||||||
// - Indent: Adds consistent indentation to text blocks
|
|
||||||
// - Write: Intelligent output that uses stdout or pager as appropriate
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Write output, using pager if configured
|
|
||||||
// if err := display.Write(output, config); err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Create faint text for de-emphasis
|
|
||||||
// fainted := display.Faint("(read-only)", config)
|
|
||||||
//
|
|
||||||
// // Indent a block of text
|
|
||||||
// indented := display.Indent(text, " ")
|
|
||||||
//
|
|
||||||
// # Color Support
|
|
||||||
//
|
|
||||||
// The package respects the colorization settings from the config.
|
|
||||||
// When colorization is disabled, formatting functions like Faint
|
|
||||||
// return unmodified text.
|
|
||||||
//
|
|
||||||
// # Terminal Detection
|
|
||||||
//
|
|
||||||
// The package uses isatty to detect if output is to a terminal,
|
|
||||||
// which affects decisions about using a pager and applying colors.
|
|
||||||
package display
|
|
||||||
@@ -2,17 +2,13 @@
|
|||||||
// cheatsheet content to stdout, or alternatively the system pager.
|
// cheatsheet content to stdout, or alternatively the system pager.
|
||||||
package display
|
package display
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Faint returns a faintly-colored string that's used to de-prioritize text
|
// Faint returns a faintly-colored string that's used to de-prioritize text
|
||||||
// written to stdout
|
// written to stdout
|
||||||
func Faint(str string, conf config.Config) string {
|
func Faint(str string, colorize bool) string {
|
||||||
// make `str` faint only if colorization has been requested
|
// make `str` faint only if colorization has been requested
|
||||||
if conf.Colorize {
|
if colorize {
|
||||||
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
package display
|
package display
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestFaint asserts that Faint applies faint formatting
|
// TestFaint asserts that Faint applies faint formatting
|
||||||
func TestFaint(t *testing.T) {
|
func TestFaint(t *testing.T) {
|
||||||
|
|
||||||
// case: apply colorization
|
// case: apply colorization
|
||||||
conf := config.Config{Colorize: true}
|
|
||||||
want := "\033[2mfoo\033[0m"
|
want := "\033[2mfoo\033[0m"
|
||||||
got := Faint("foo", conf)
|
got := Faint("foo", true)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: do not apply colorization
|
// case: do not apply colorization
|
||||||
conf.Colorize = false
|
|
||||||
want = "foo"
|
want = "foo"
|
||||||
got = Faint("foo", conf)
|
got = Faint("foo", false)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func Write(out string, conf config.Config) {
|
|||||||
|
|
||||||
// writeToPager writes output through a pager command
|
// writeToPager writes output through a pager command
|
||||||
func writeToPager(out string, conf config.Config) {
|
func writeToPager(out string, conf config.Config) {
|
||||||
parts := strings.Split(conf.Pager, " ")
|
parts := strings.Fields(conf.Pager)
|
||||||
pager := parts[0]
|
pager := parts[0]
|
||||||
args := parts[1:]
|
args := parts[1:]
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package installer
|
package installer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,20 +11,34 @@ import (
|
|||||||
// Prompt prompts the user for a answer
|
// Prompt prompts the user for a answer
|
||||||
func Prompt(prompt string, def bool) (bool, error) {
|
func Prompt(prompt string, def bool) (bool, error) {
|
||||||
|
|
||||||
// initialize a line reader
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
// display the prompt
|
// display the prompt
|
||||||
fmt.Printf("%s: ", prompt)
|
fmt.Printf("%s: ", prompt)
|
||||||
|
|
||||||
// read the answer
|
// read one byte at a time until newline to avoid buffering past the
|
||||||
ans, err := reader.ReadString('\n')
|
// end of the current line, which would consume input intended for
|
||||||
if err != nil {
|
// subsequent Prompt calls on the same stdin
|
||||||
return false, fmt.Errorf("failed to parse input: %v", err)
|
var line []byte
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
n, err := os.Stdin.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if buf[0] != '\r' {
|
||||||
|
line = append(line, buf[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if len(line) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("failed to prompt: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalize the answer
|
// normalize the answer
|
||||||
ans = strings.ToLower(strings.TrimSpace(ans))
|
ans := strings.ToLower(strings.TrimSpace(string(line)))
|
||||||
|
|
||||||
// return the appropriate response
|
// return the appropriate response
|
||||||
switch ans {
|
switch ans {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package installer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -154,27 +153,7 @@ func TestPromptError(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error when reading from closed stdin, got nil")
|
t.Error("expected error when reading from closed stdin, got nil")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "failed to parse input") {
|
if !strings.Contains(err.Error(), "failed to prompt") {
|
||||||
t.Errorf("expected 'failed to parse input' error, got: %v", err)
|
t.Errorf("expected 'failed to prompt' error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPromptIntegration provides a simple integration test
|
|
||||||
func TestPromptIntegration(t *testing.T) {
|
|
||||||
// This demonstrates how the prompt would be used in practice
|
|
||||||
// It's skipped by default since it requires actual user input
|
|
||||||
if os.Getenv("TEST_INTERACTIVE") != "1" {
|
|
||||||
t.Skip("Skipping interactive test - set TEST_INTERACTIVE=1 to run")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\n=== Interactive Prompt Test ===")
|
|
||||||
fmt.Println("You will be prompted to answer a question.")
|
|
||||||
fmt.Println("Try different inputs: y, n, Y, N, empty (just press Enter)")
|
|
||||||
|
|
||||||
result, err := Prompt("Would you like to continue? [Y/n]", true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Prompt failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("You answered: %v\n", result)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package installer
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/repo"
|
"github.com/cheat/cheat/internal/repo"
|
||||||
@@ -13,25 +11,11 @@ import (
|
|||||||
// Run runs the installer
|
// Run runs the installer
|
||||||
func Run(configs string, confpath string) error {
|
func Run(configs string, confpath string) error {
|
||||||
|
|
||||||
// determine the appropriate paths for config data and (optional) community
|
// expand template placeholders with platform-appropriate paths
|
||||||
// cheatsheets based on the user's platform
|
configs = ExpandTemplate(configs, confpath)
|
||||||
confdir := filepath.Dir(confpath)
|
|
||||||
|
|
||||||
// create paths for community and personal cheatsheets
|
// determine cheatsheet directory paths
|
||||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
community, personal, work := cheatsheetDirs(confpath)
|
||||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
|
||||||
|
|
||||||
// set default cheatpaths
|
|
||||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
|
||||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
|
|
||||||
|
|
||||||
// locate and set a default pager
|
|
||||||
configs = strings.Replace(configs, "PAGER_PATH", config.Pager(), -1)
|
|
||||||
|
|
||||||
// locate and set a default editor
|
|
||||||
if editor, err := config.Editor(); err == nil {
|
|
||||||
configs = strings.Replace(configs, "EDITOR_PATH", editor, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompt the user to download the community cheatsheets
|
// prompt the user to download the community cheatsheets
|
||||||
yes, err := Prompt(
|
yes, err := Prompt(
|
||||||
@@ -44,15 +28,17 @@ func Run(configs string, confpath string) error {
|
|||||||
|
|
||||||
// clone the community cheatsheets if so instructed
|
// clone the community cheatsheets if so instructed
|
||||||
if yes {
|
if yes {
|
||||||
// clone the community cheatsheets
|
|
||||||
fmt.Printf("Cloning community cheatsheets to %s.\n", community)
|
fmt.Printf("Cloning community cheatsheets to %s.\n", community)
|
||||||
if err := repo.Clone(community); err != nil {
|
if err := repo.Clone(community); err != nil {
|
||||||
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
configs = CommentCommunity(configs, confpath)
|
||||||
|
}
|
||||||
|
|
||||||
// also create a directory for personal cheatsheets
|
// always create personal and work directories
|
||||||
fmt.Printf("Cloning personal cheatsheets to %s.\n", personal)
|
for _, dir := range []string{personal, work} {
|
||||||
if err := os.MkdirAll(personal, os.ModePerm); err != nil {
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package installer
|
package installer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -53,8 +53,8 @@ cheatpaths:
|
|||||||
confpath: filepath.Join(tempDir, "conf1", "conf.yml"),
|
confpath: filepath.Join(tempDir, "conf1", "conf.yml"),
|
||||||
userInput: "n\n",
|
userInput: "n\n",
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
checkFiles: []string{"conf1/conf.yml"},
|
checkFiles: []string{"conf1/conf.yml", "conf1/cheatsheets/personal", "conf1/cheatsheets/work"},
|
||||||
dontWantFiles: []string{"conf1/cheatsheets/community", "conf1/cheatsheets/personal"},
|
dontWantFiles: []string{"conf1/cheatsheets/community"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user accepts but clone fails",
|
name: "user accepts but clone fails",
|
||||||
@@ -69,15 +69,33 @@ cheatpaths:
|
|||||||
wantInErr: "failed to clone cheatsheets",
|
wantInErr: "failed to clone cheatsheets",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid config path",
|
name: "invalid config path",
|
||||||
configs: "test",
|
configs: "test",
|
||||||
confpath: "/nonexistent/path/conf.yml",
|
// /dev/null/... is truly uncreatable on Unix;
|
||||||
|
// NUL\... is uncreatable on Windows
|
||||||
|
confpath: func() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return `NUL\impossible\conf.yml`
|
||||||
|
}
|
||||||
|
return "/dev/null/impossible/conf.yml"
|
||||||
|
}(),
|
||||||
userInput: "n\n",
|
userInput: "n\n",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantInErr: "failed to create config file",
|
wantInErr: "failed to create",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-create a .git dir inside the community path so go-git's PlainClone
|
||||||
|
// returns ErrRepositoryAlreadyExists (otherwise, on CI runners with
|
||||||
|
// network access, the real clone succeeds and the test fails)
|
||||||
|
fakeGitDir := filepath.Join(tempDir, "conf2", "cheatsheets", "community", ".git")
|
||||||
|
if err := os.MkdirAll(fakeGitDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create fake .git dir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(fakeGitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write fake HEAD: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create stdin pipe
|
// Create stdin pipe
|
||||||
@@ -158,10 +176,18 @@ func TestRunStringReplacements(t *testing.T) {
|
|||||||
editor: EDITOR_PATH
|
editor: EDITOR_PATH
|
||||||
pager: PAGER_PATH
|
pager: PAGER_PATH
|
||||||
cheatpaths:
|
cheatpaths:
|
||||||
- name: community
|
|
||||||
path: COMMUNITY_PATH
|
|
||||||
- name: personal
|
- name: personal
|
||||||
path: PERSONAL_PATH
|
path: PERSONAL_PATH
|
||||||
|
tags: [ personal ]
|
||||||
|
readonly: false
|
||||||
|
- name: work
|
||||||
|
path: WORK_PATH
|
||||||
|
tags: [ work ]
|
||||||
|
readonly: false
|
||||||
|
- name: community
|
||||||
|
path: COMMUNITY_PATH
|
||||||
|
tags: [ community ]
|
||||||
|
readonly: true
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create temp directory
|
// Create temp directory
|
||||||
@@ -175,7 +201,6 @@ cheatpaths:
|
|||||||
confdir := filepath.Dir(confpath)
|
confdir := filepath.Dir(confpath)
|
||||||
|
|
||||||
// Expected paths
|
// Expected paths
|
||||||
expectedCommunity := filepath.Join(confdir, "cheatsheets", "community")
|
|
||||||
expectedPersonal := filepath.Join(confdir, "cheatsheets", "personal")
|
expectedPersonal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||||
|
|
||||||
// Save original stdin/stdout
|
// Save original stdin/stdout
|
||||||
@@ -219,16 +244,22 @@ cheatpaths:
|
|||||||
if strings.Contains(contentStr, "PERSONAL_PATH") {
|
if strings.Contains(contentStr, "PERSONAL_PATH") {
|
||||||
t.Error("PERSONAL_PATH was not replaced")
|
t.Error("PERSONAL_PATH was not replaced")
|
||||||
}
|
}
|
||||||
if strings.Contains(contentStr, "EDITOR_PATH") && !strings.Contains(contentStr, fmt.Sprintf("editor: %s", "")) {
|
if strings.Contains(contentStr, "EDITOR_PATH") {
|
||||||
t.Error("EDITOR_PATH was not replaced")
|
t.Error("EDITOR_PATH was not replaced")
|
||||||
}
|
}
|
||||||
if strings.Contains(contentStr, "PAGER_PATH") && !strings.Contains(contentStr, fmt.Sprintf("pager: %s", "")) {
|
if strings.Contains(contentStr, "PAGER_PATH") {
|
||||||
t.Error("PAGER_PATH was not replaced")
|
t.Error("PAGER_PATH was not replaced")
|
||||||
}
|
}
|
||||||
|
if strings.Contains(contentStr, "WORK_PATH") {
|
||||||
|
t.Error("WORK_PATH was not replaced")
|
||||||
|
}
|
||||||
|
|
||||||
// Verify correct paths were used
|
// Verify community path is commented out (user declined)
|
||||||
if !strings.Contains(contentStr, expectedCommunity) {
|
if strings.Contains(contentStr, " - name: community") {
|
||||||
t.Errorf("expected community path %q in config", expectedCommunity)
|
t.Error("expected community cheatpath to be commented out when declined")
|
||||||
|
}
|
||||||
|
if !strings.Contains(contentStr, " #- name: community") {
|
||||||
|
t.Error("expected commented-out community cheatpath")
|
||||||
}
|
}
|
||||||
if !strings.Contains(contentStr, expectedPersonal) {
|
if !strings.Contains(contentStr, expectedPersonal) {
|
||||||
t.Errorf("expected personal path %q in config", expectedPersonal)
|
t.Errorf("expected personal path %q in config", expectedPersonal)
|
||||||
|
|||||||
58
internal/installer/template.go
Normal file
58
internal/installer/template.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cheatsheetDirs returns the community, personal, and work cheatsheet
|
||||||
|
// directory paths derived from a config file path.
|
||||||
|
func cheatsheetDirs(confpath string) (community, personal, work string) {
|
||||||
|
confdir := filepath.Dir(confpath)
|
||||||
|
community = filepath.Join(confdir, "cheatsheets", "community")
|
||||||
|
personal = filepath.Join(confdir, "cheatsheets", "personal")
|
||||||
|
work = filepath.Join(confdir, "cheatsheets", "work")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandTemplate replaces placeholder tokens in the config template with
|
||||||
|
// platform-appropriate paths derived from confpath.
|
||||||
|
func ExpandTemplate(configs string, confpath string) string {
|
||||||
|
community, personal, work := cheatsheetDirs(confpath)
|
||||||
|
|
||||||
|
// substitute paths
|
||||||
|
configs = strings.ReplaceAll(configs, "COMMUNITY_PATH", community)
|
||||||
|
configs = strings.ReplaceAll(configs, "PERSONAL_PATH", personal)
|
||||||
|
configs = strings.ReplaceAll(configs, "WORK_PATH", work)
|
||||||
|
|
||||||
|
// locate and set a default pager
|
||||||
|
configs = strings.ReplaceAll(configs, "PAGER_PATH", config.Pager())
|
||||||
|
|
||||||
|
// locate and set a default editor
|
||||||
|
if editor, err := config.Editor(); err == nil {
|
||||||
|
configs = strings.ReplaceAll(configs, "EDITOR_PATH", editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentCommunity comments out the community cheatpath block in the config
|
||||||
|
// template. This is used when the community cheatsheets directory won't exist
|
||||||
|
// (either because the user declined to download them, or because the config
|
||||||
|
// is being output as an example).
|
||||||
|
func CommentCommunity(configs string, confpath string) string {
|
||||||
|
community, _, _ := cheatsheetDirs(confpath)
|
||||||
|
|
||||||
|
return strings.ReplaceAll(configs,
|
||||||
|
" - name: community\n"+
|
||||||
|
" path: "+community+"\n"+
|
||||||
|
" tags: [ community ]\n"+
|
||||||
|
" readonly: true",
|
||||||
|
" #- name: community\n"+
|
||||||
|
" # path: "+community+"\n"+
|
||||||
|
" # tags: [ community ]\n"+
|
||||||
|
" # readonly: true",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Package mock implements mock functions used in unit-tests.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Path returns the absolute path to the specified mock file.
|
|
||||||
func Path(filename string) string {
|
|
||||||
|
|
||||||
// determine the path of this file during runtime
|
|
||||||
_, thisfile, _, _ := runtime.Caller(0)
|
|
||||||
|
|
||||||
// compute the mock path
|
|
||||||
file, err := filepath.Abs(
|
|
||||||
path.Join(
|
|
||||||
filepath.Dir(thisfile),
|
|
||||||
"../../mocks",
|
|
||||||
filename,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ func TestClone(t *testing.T) {
|
|||||||
// that don't require actual cloning
|
// that don't require actual cloning
|
||||||
|
|
||||||
t.Run("clone to read-only directory", func(t *testing.T) {
|
t.Run("clone to read-only directory", func(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("chmod does not restrict writes on Windows")
|
||||||
|
}
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
t.Skip("Cannot test read-only directory as root")
|
t.Skip("Cannot test read-only directory as root")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// gitSep is the `.git` path component surrounded by path separators.
|
||||||
|
// Used to match `.git` as a complete path component, not as a suffix
|
||||||
|
// of a directory name (e.g., `personal.git`).
|
||||||
|
var gitSep = string(os.PathSeparator) + ".git" + string(os.PathSeparator)
|
||||||
|
|
||||||
// GitDir returns `true` if we are iterating over a directory contained within
|
// GitDir returns `true` if we are iterating over a directory contained within
|
||||||
// a repositories `.git` directory.
|
// a repositories `.git` directory.
|
||||||
func GitDir(path string) (bool, error) {
|
func GitDir(path string) (bool, error) {
|
||||||
@@ -50,9 +55,20 @@ func GitDir(path string) (bool, error) {
|
|||||||
|
|
||||||
See: https://github.com/cheat/cheat/issues/699
|
See: https://github.com/cheat/cheat/issues/699
|
||||||
|
|
||||||
Accounting for all of the above (hopefully?), the current solution is
|
Accounting for all of the above, the next solution was to search not
|
||||||
not to search for `.git`, but `.git/` (including the directory
|
for `.git`, but `.git/` (including the directory separator), and then
|
||||||
separator), and then only ceasing to walk the directory on a match.
|
only ceasing to walk the directory on a match.
|
||||||
|
|
||||||
|
This, however, also had a bug: searching for `.git/` also matched
|
||||||
|
directory names that *ended with* `.git`, like `personal.git/`. This
|
||||||
|
caused cheatsheets stored under such paths to be silently skipped.
|
||||||
|
|
||||||
|
See: https://github.com/cheat/cheat/issues/711
|
||||||
|
|
||||||
|
The current (and hopefully final) solution requires the path separator
|
||||||
|
on *both* sides of `.git`, i.e., searching for `/.git/`. This ensures
|
||||||
|
that `.git` is matched only as a complete path component, not as a
|
||||||
|
suffix of a directory name.
|
||||||
|
|
||||||
To summarize, this code must account for the following possibilities:
|
To summarize, this code must account for the following possibilities:
|
||||||
|
|
||||||
@@ -61,17 +77,16 @@ func GitDir(path string) (bool, error) {
|
|||||||
3. A cheatpath is a repository, and contains a `.git*` file
|
3. A cheatpath is a repository, and contains a `.git*` file
|
||||||
4. A cheatpath is a submodule
|
4. A cheatpath is a submodule
|
||||||
5. A cheatpath is a hidden directory
|
5. A cheatpath is a hidden directory
|
||||||
|
6. A cheatpath is inside a directory whose name ends with `.git`
|
||||||
|
|
||||||
Care must be taken to support the above on both Unix and Windows
|
Care must be taken to support the above on both Unix and Windows
|
||||||
systems, which have different directory separators and line-endings.
|
systems, which have different directory separators and line-endings.
|
||||||
|
|
||||||
There is a lot of nuance to all of this, and it would be worthwhile to
|
NB: `filepath.Walk` always passes absolute paths to the walk function,
|
||||||
do two things to stop writing bugs here:
|
so `.git` will never appear as the first path component. This is what
|
||||||
|
makes the "separator on both sides" approach safe.
|
||||||
|
|
||||||
1. Build integration tests around all of this
|
A reasonable smoke-test for ensuring that skipping is being applied
|
||||||
2. Discard string-matching solutions entirely, and use `go-git` instead
|
|
||||||
|
|
||||||
NB: A reasonable smoke-test for ensuring that skipping is being applied
|
|
||||||
correctly is to run the following command:
|
correctly is to run the following command:
|
||||||
|
|
||||||
make && strace ./dist/cheat -l | wc -l
|
make && strace ./dist/cheat -l | wc -l
|
||||||
@@ -83,8 +98,8 @@ func GitDir(path string) (bool, error) {
|
|||||||
of syscalls should be significantly lower with the skip check enabled.
|
of syscalls should be significantly lower with the skip check enabled.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// determine if the literal string `.git` appears within `path`
|
// determine if `.git` appears as a complete path component
|
||||||
pos := strings.Index(path, fmt.Sprintf(".git%s", string(os.PathSeparator)))
|
pos := strings.Index(path, gitSep)
|
||||||
|
|
||||||
// if it does not, we know for certain that we are not within a `.git`
|
// if it does not, we know for certain that we are not within a `.git`
|
||||||
// directory.
|
// directory.
|
||||||
|
|||||||
@@ -1,137 +1,191 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGitDir(t *testing.T) {
|
// setupGitDirTestTree creates a temporary directory structure that exercises
|
||||||
// Create a temporary directory for testing
|
// every case documented in GitDir's comment block. The caller must defer
|
||||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
// os.RemoveAll on the returned root.
|
||||||
if err != nil {
|
//
|
||||||
t.Fatalf("failed to create temp dir: %v", err)
|
// Layout:
|
||||||
}
|
//
|
||||||
defer os.RemoveAll(tempDir)
|
// root/
|
||||||
|
// ├── plain/ # not a repository
|
||||||
|
// │ └── sheet
|
||||||
|
// ├── repo/ # a repository (.git is a directory)
|
||||||
|
// │ ├── .git/
|
||||||
|
// │ │ ├── HEAD
|
||||||
|
// │ │ ├── objects/
|
||||||
|
// │ │ │ └── pack/
|
||||||
|
// │ │ └── refs/
|
||||||
|
// │ │ └── heads/
|
||||||
|
// │ ├── .gitignore
|
||||||
|
// │ ├── .gitattributes
|
||||||
|
// │ └── sheet
|
||||||
|
// ├── submodule/ # a submodule (.git is a file)
|
||||||
|
// │ ├── .git # file, not directory
|
||||||
|
// │ └── sheet
|
||||||
|
// ├── dotgit-suffix.git/ # directory name ends in .git (#711)
|
||||||
|
// │ └── cheat/
|
||||||
|
// │ └── sheet
|
||||||
|
// ├── dotgit-mid.git/ # .git suffix mid-path (#711)
|
||||||
|
// │ └── nested/
|
||||||
|
// │ └── sheet
|
||||||
|
// ├── .github/ # .github directory (not .git)
|
||||||
|
// │ └── workflows/
|
||||||
|
// │ └── ci.yml
|
||||||
|
// └── .hidden/ # generic hidden directory
|
||||||
|
// └── sheet
|
||||||
|
func setupGitDirTestTree(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
// Create test directory structure
|
root := t.TempDir()
|
||||||
testDirs := []string{
|
|
||||||
filepath.Join(tempDir, ".git"),
|
dirs := []string{
|
||||||
filepath.Join(tempDir, ".git", "objects"),
|
// case 1: not a repository
|
||||||
filepath.Join(tempDir, ".git", "refs"),
|
filepath.Join(root, "plain"),
|
||||||
filepath.Join(tempDir, "regular"),
|
|
||||||
filepath.Join(tempDir, "regular", ".git"),
|
// case 2: a repository (.git directory with contents)
|
||||||
filepath.Join(tempDir, "submodule"),
|
filepath.Join(root, "repo", ".git", "objects", "pack"),
|
||||||
|
filepath.Join(root, "repo", ".git", "refs", "heads"),
|
||||||
|
|
||||||
|
// case 4: a submodule (.git is a file)
|
||||||
|
filepath.Join(root, "submodule"),
|
||||||
|
|
||||||
|
// case 6: directory name ending in .git (#711)
|
||||||
|
filepath.Join(root, "dotgit-suffix.git", "cheat"),
|
||||||
|
filepath.Join(root, "dotgit-mid.git", "nested"),
|
||||||
|
|
||||||
|
// .github (should not be confused with .git)
|
||||||
|
filepath.Join(root, ".github", "workflows"),
|
||||||
|
|
||||||
|
// generic hidden directory
|
||||||
|
filepath.Join(root, ".hidden"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range testDirs {
|
for _, dir := range dirs {
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
t.Fatalf("failed to create dir %s: %v", dir, err)
|
t.Fatalf("failed to create dir %s: %v", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create test files
|
files := map[string]string{
|
||||||
testFiles := map[string]string{
|
// sheets
|
||||||
filepath.Join(tempDir, ".gitignore"): "*.tmp\n",
|
filepath.Join(root, "plain", "sheet"): "plain sheet",
|
||||||
filepath.Join(tempDir, ".gitattributes"): "* text=auto\n",
|
filepath.Join(root, "repo", "sheet"): "repo sheet",
|
||||||
filepath.Join(tempDir, "submodule", ".git"): "gitdir: ../.git/modules/submodule\n",
|
filepath.Join(root, "submodule", "sheet"): "submod sheet",
|
||||||
filepath.Join(tempDir, "regular", "sheet.txt"): "content\n",
|
filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"): "dotgit sheet",
|
||||||
|
filepath.Join(root, "dotgit-mid.git", "nested", "sheet"): "dotgit nested",
|
||||||
|
filepath.Join(root, ".hidden", "sheet"): "hidden sheet",
|
||||||
|
|
||||||
|
// git metadata
|
||||||
|
filepath.Join(root, "repo", ".git", "HEAD"): "ref: refs/heads/main\n",
|
||||||
|
filepath.Join(root, "repo", ".gitignore"): "*.tmp\n",
|
||||||
|
filepath.Join(root, "repo", ".gitattributes"): "* text=auto\n",
|
||||||
|
filepath.Join(root, "submodule", ".git"): "gitdir: ../.git/modules/sub\n",
|
||||||
|
filepath.Join(root, ".github", "workflows", "ci.yml"): "name: CI\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
for file, content := range testFiles {
|
for path, content := range files {
|
||||||
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||||
t.Fatalf("failed to create file %s: %v", file, err)
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
return root
|
||||||
name string
|
|
||||||
path string
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "not in git directory",
|
|
||||||
path: filepath.Join(tempDir, "regular", "sheet.txt"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "in .git directory",
|
|
||||||
path: filepath.Join(tempDir, ".git", "objects", "file"),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "in .git/refs directory",
|
|
||||||
path: filepath.Join(tempDir, ".git", "refs", "heads", "main"),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: ".gitignore file",
|
|
||||||
path: filepath.Join(tempDir, ".gitignore"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: ".gitattributes file",
|
|
||||||
path: filepath.Join(tempDir, ".gitattributes"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "submodule with .git file",
|
|
||||||
path: filepath.Join(tempDir, "submodule", "sheet.txt"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path with .git in middle",
|
|
||||||
path: filepath.Join(tempDir, "regular", ".git", "sheet.txt"),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nonexistent path without .git",
|
|
||||||
path: filepath.Join(tempDir, "nonexistent", "file"),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := GitDir(tt.path)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("GitDir() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("GitDir() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitDirEdgeCases(t *testing.T) {
|
func TestGitDir(t *testing.T) {
|
||||||
// Test with paths that have .git but not as a directory separator
|
root := setupGitDirTestTree(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
|
// Case 1: not a repository — no .git anywhere in path
|
||||||
{
|
{
|
||||||
name: "file ending with .git",
|
name: "plain directory, no repo",
|
||||||
path: "/tmp/myfile.git",
|
path: filepath.Join(root, "plain", "sheet"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Case 2: a repository — paths *inside* .git/ should be detected
|
||||||
|
{
|
||||||
|
name: "inside .git directory",
|
||||||
|
path: filepath.Join(root, "repo", ".git", "HEAD"),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inside .git/objects",
|
||||||
|
path: filepath.Join(root, "repo", ".git", "objects", "pack", "somefile"),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inside .git/refs",
|
||||||
|
path: filepath.Join(root, "repo", ".git", "refs", "heads", "main"),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Case 2 (cont.): files *alongside* .git should NOT be detected
|
||||||
|
{
|
||||||
|
name: "sheet in repo root (beside .git dir)",
|
||||||
|
path: filepath.Join(root, "repo", "sheet"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Case 3: .git* files (like .gitignore) should NOT trigger
|
||||||
|
{
|
||||||
|
name: ".gitignore file",
|
||||||
|
path: filepath.Join(root, "repo", ".gitignore"),
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "directory ending with .git",
|
name: ".gitattributes file",
|
||||||
path: "/tmp/myrepo.git",
|
path: filepath.Join(root, "repo", ".gitattributes"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Case 4: submodule — .git is a file, not a directory
|
||||||
|
{
|
||||||
|
name: "sheet in submodule (where .git is a file)",
|
||||||
|
path: filepath.Join(root, "submodule", "sheet"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Case 6: directory name ends with .git (#711)
|
||||||
|
{
|
||||||
|
name: "sheet under directory ending in .git",
|
||||||
|
path: filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"),
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: ".github directory",
|
name: "sheet under .git-suffixed dir, nested deeper",
|
||||||
path: "/tmp/.github/workflows",
|
path: filepath.Join(root, "dotgit-mid.git", "nested", "sheet"),
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// .github directory — must not be confused with .git
|
||||||
{
|
{
|
||||||
name: "legitimate.git-repo name",
|
name: "file inside .github directory",
|
||||||
path: "/tmp/legitimate.git-repo/file",
|
path: filepath.Join(root, ".github", "workflows", "ci.yml"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hidden directory that is not .git
|
||||||
|
{
|
||||||
|
name: "file inside generic hidden directory",
|
||||||
|
path: filepath.Join(root, ".hidden", "sheet"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Path with no .git at all
|
||||||
|
{
|
||||||
|
name: "path with no .git component whatsoever",
|
||||||
|
path: filepath.Join(root, "nonexistent", "file"),
|
||||||
want: false,
|
want: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -140,8 +194,7 @@ func TestGitDirEdgeCases(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := GitDir(tt.path)
|
got, err := GitDir(tt.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// It's ok if the path doesn't exist for these edge case tests
|
t.Fatalf("GitDir(%q) returned unexpected error: %v", tt.path, err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if got != tt.want {
|
||||||
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
||||||
@@ -150,28 +203,153 @@ func TestGitDirEdgeCases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitDirPathSeparator(t *testing.T) {
|
// TestGitDirWithNestedGitDir tests a repo inside a .git-suffixed parent
|
||||||
// Test that the function correctly uses os.PathSeparator
|
// directory. This is the nastiest combination: a real .git directory that
|
||||||
// This is important for cross-platform compatibility
|
// appears *after* a .git suffix in the path.
|
||||||
|
func TestGitDirWithNestedGitDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
// Create a path with the wrong separator for the current OS
|
// Create: root/cheats.git/repo/.git/HEAD
|
||||||
var wrongSep string
|
// root/cheats.git/repo/sheet
|
||||||
if os.PathSeparator == '/' {
|
gitDir := filepath.Join(root, "cheats.git", "repo", ".git")
|
||||||
wrongSep = `\`
|
if err := os.MkdirAll(gitDir, 0755); err != nil {
|
||||||
} else {
|
t.Fatal(err)
|
||||||
wrongSep = `/`
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "cheats.git", "repo", "sheet"), []byte("content"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path with wrong separator should not be detected as git dir
|
tests := []struct {
|
||||||
path := fmt.Sprintf("some%spath%s.git%sfile", wrongSep, wrongSep, wrongSep)
|
name string
|
||||||
isGit, err := GitDir(path)
|
path string
|
||||||
|
want bool
|
||||||
if err != nil {
|
}{
|
||||||
// Path doesn't exist, which is fine
|
{
|
||||||
return
|
name: "sheet beside .git in .git-suffixed parent",
|
||||||
|
path: filepath.Join(root, "cheats.git", "repo", "sheet"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file inside .git inside .git-suffixed parent",
|
||||||
|
path: filepath.Join(root, "cheats.git", "repo", ".git", "HEAD"),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if isGit {
|
for _, tt := range tests {
|
||||||
t.Errorf("GitDir() incorrectly detected git dir with wrong path separator")
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GitDir(tt.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GitDir(%q) returned unexpected error: %v", tt.path, err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGitDirSubmoduleInsideDotGitSuffix tests a submodule (.git file)
|
||||||
|
// inside a .git-suffixed parent directory.
|
||||||
|
func TestGitDirSubmoduleInsideDotGitSuffix(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
// Create: root/personal.git/submod/.git (file)
|
||||||
|
// root/personal.git/submod/sheet
|
||||||
|
subDir := filepath.Join(root, "personal.git", "submod")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// .git as a file (submodule pointer)
|
||||||
|
if err := os.WriteFile(filepath.Join(subDir, ".git"), []byte("gitdir: ../../.git/modules/sub\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(subDir, "sheet"), []byte("content"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := GitDir(filepath.Join(subDir, "sheet"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if got {
|
||||||
|
t.Error("GitDir should return false for sheet in submodule under .git-suffixed parent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGitDirIntegrationWalk simulates what sheets.Load does: walking a
|
||||||
|
// directory tree and checking each path with GitDir. This verifies that
|
||||||
|
// the function works correctly in the context of filepath.Walk, which is
|
||||||
|
// how it is actually called.
|
||||||
|
func TestGitDirIntegrationWalk(t *testing.T) {
|
||||||
|
root := setupGitDirTestTree(t)
|
||||||
|
|
||||||
|
// Walk the tree and collect which paths GitDir says to skip
|
||||||
|
var skipped []string
|
||||||
|
var visited []string
|
||||||
|
|
||||||
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isGit, err := GitDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isGit {
|
||||||
|
skipped = append(skipped, path)
|
||||||
|
} else {
|
||||||
|
visited = append(visited, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Walk failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files inside .git/ should be skipped
|
||||||
|
expectSkipped := []string{
|
||||||
|
filepath.Join(root, "repo", ".git", "HEAD"),
|
||||||
|
}
|
||||||
|
for _, want := range expectSkipped {
|
||||||
|
found := false
|
||||||
|
for _, got := range skipped {
|
||||||
|
if got == want {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected %q to be skipped, but it was not", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sheets should NOT be skipped — including the #711 case
|
||||||
|
expectVisited := []string{
|
||||||
|
filepath.Join(root, "plain", "sheet"),
|
||||||
|
filepath.Join(root, "repo", "sheet"),
|
||||||
|
filepath.Join(root, "submodule", "sheet"),
|
||||||
|
filepath.Join(root, "dotgit-suffix.git", "cheat", "sheet"),
|
||||||
|
filepath.Join(root, "dotgit-mid.git", "nested", "sheet"),
|
||||||
|
filepath.Join(root, ".hidden", "sheet"),
|
||||||
|
}
|
||||||
|
for _, want := range expectVisited {
|
||||||
|
found := false
|
||||||
|
for _, got := range visited {
|
||||||
|
if got == want {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected %q to be visited (not skipped), but it was not found in visited paths", want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package repo
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
@@ -16,45 +17,26 @@ func TestColorize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mock a sheet
|
// mock a sheet
|
||||||
|
original := "echo 'foo'"
|
||||||
s := Sheet{
|
s := Sheet{
|
||||||
Text: "echo 'foo'",
|
Text: original,
|
||||||
}
|
}
|
||||||
|
|
||||||
// colorize the sheet text
|
// colorize the sheet text
|
||||||
s.Colorize(conf)
|
s.Colorize(conf)
|
||||||
|
|
||||||
// initialize expectations
|
// assert that the text was modified (colorization applied)
|
||||||
want := "[38;2;181;137;0mecho[0m[38;2;147;161;161m"
|
if s.Text == original {
|
||||||
want += " [0m[38;2;42;161;152m'foo'[0m"
|
t.Error("Colorize did not modify sheet text")
|
||||||
|
}
|
||||||
|
|
||||||
// assert
|
// assert that ANSI escape codes are present
|
||||||
if s.Text != want {
|
if !strings.Contains(s.Text, "\x1b[") && !strings.Contains(s.Text, "[0m") {
|
||||||
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text)
|
t.Errorf("colorized text does not contain ANSI escape codes: %q", s.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert that the original content is still present within the colorized output
|
||||||
|
if !strings.Contains(s.Text, "echo") || !strings.Contains(s.Text, "foo") {
|
||||||
|
t.Errorf("colorized text lost original content: %q", s.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestColorizeError tests the error handling in Colorize
|
|
||||||
func TestColorizeError(_ *testing.T) {
|
|
||||||
// Create a sheet with content
|
|
||||||
sheet := Sheet{
|
|
||||||
Text: "some text",
|
|
||||||
Syntax: "invalidlexer12345", // Use an invalid lexer that might cause issues
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a config with invalid formatter/style
|
|
||||||
conf := config.Config{
|
|
||||||
Formatter: "invalidformatter",
|
|
||||||
Style: "invalidstyle",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store original text
|
|
||||||
originalText := sheet.Text
|
|
||||||
|
|
||||||
// Colorize should not panic even with invalid settings
|
|
||||||
sheet.Colorize(conf)
|
|
||||||
|
|
||||||
// The text might be unchanged if there was an error, or it might be colorized
|
|
||||||
// We're mainly testing that it doesn't panic
|
|
||||||
_ = sheet.Text
|
|
||||||
_ = originalText
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,21 +3,19 @@ package sheet
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCopyErrors tests error cases for the Copy method
|
// TestCopyErrors tests error cases for the Copy method
|
||||||
func TestCopyErrors(t *testing.T) {
|
func TestCopyErrors(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
setup func() (*Sheet, string, func())
|
setup func() (*Sheet, string, func())
|
||||||
wantErr bool
|
|
||||||
errMsg string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "source file does not exist",
|
name: "source file does not exist",
|
||||||
setup: func() (*Sheet, string, func()) {
|
setup: func() (*Sheet, string, func()) {
|
||||||
// Create a sheet with non-existent path
|
|
||||||
sheet := &Sheet{
|
sheet := &Sheet{
|
||||||
Title: "test",
|
Title: "test",
|
||||||
Path: "/non/existent/file.txt",
|
Path: "/non/existent/file.txt",
|
||||||
@@ -29,13 +27,10 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return sheet, dest, cleanup
|
return sheet, dest, cleanup
|
||||||
},
|
},
|
||||||
wantErr: true,
|
|
||||||
errMsg: "failed to open cheatsheet",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "destination directory creation fails",
|
name: "destination directory creation fails",
|
||||||
setup: func() (*Sheet, string, func()) {
|
setup: func() (*Sheet, string, func()) {
|
||||||
// Create a source file
|
|
||||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp file: %v", err)
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
@@ -49,13 +44,11 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
CheatPath: "test",
|
CheatPath: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a file where we want a directory
|
|
||||||
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
|
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
|
||||||
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
|
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
|
||||||
t.Fatalf("failed to create blocker file: %v", err)
|
t.Fatalf("failed to create blocker file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to create dest under the blocker file (will fail)
|
|
||||||
dest := filepath.Join(blockerFile, "subdir", "dest.txt")
|
dest := filepath.Join(blockerFile, "subdir", "dest.txt")
|
||||||
|
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
@@ -64,13 +57,10 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return sheet, dest, cleanup
|
return sheet, dest, cleanup
|
||||||
},
|
},
|
||||||
wantErr: true,
|
|
||||||
errMsg: "failed to create directory",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "destination file creation fails",
|
name: "destination file creation fails",
|
||||||
setup: func() (*Sheet, string, func()) {
|
setup: func() (*Sheet, string, func()) {
|
||||||
// Create a source file
|
|
||||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp file: %v", err)
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
@@ -84,7 +74,6 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
CheatPath: "test",
|
CheatPath: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a directory where we want the file
|
|
||||||
destDir := filepath.Join(os.TempDir(), "copy-test-dir")
|
destDir := filepath.Join(os.TempDir(), "copy-test-dir")
|
||||||
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
|
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
|
||||||
t.Fatalf("failed to create dest dir: %v", err)
|
t.Fatalf("failed to create dest dir: %v", err)
|
||||||
@@ -96,8 +85,6 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return sheet, destDir, cleanup
|
return sheet, destDir, cleanup
|
||||||
},
|
},
|
||||||
wantErr: true,
|
|
||||||
errMsg: "failed to create outfile",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,39 +94,27 @@ func TestCopyErrors(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
err := sheet.Copy(dest)
|
err := sheet.Copy(dest)
|
||||||
if (err != nil) != tt.wantErr {
|
if err == nil {
|
||||||
t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr)
|
t.Error("Copy() expected error, got nil")
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil && tt.errMsg != "" {
|
|
||||||
if !contains(err.Error(), tt.errMsg) {
|
|
||||||
t.Errorf("Copy() error = %v, want error containing %q", err, tt.errMsg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCopyIOError tests the io.Copy error case
|
// TestCopyUnreadableSource verifies that Copy returns an error when the source
|
||||||
func TestCopyIOError(t *testing.T) {
|
// file cannot be opened (e.g., permission denied).
|
||||||
// This is difficult to test without mocking io.Copy
|
func TestCopyUnreadableSource(t *testing.T) {
|
||||||
// The error case would occur if the source file is modified
|
if runtime.GOOS == "windows" {
|
||||||
// or removed after opening but before copying
|
t.Skip("chmod does not restrict reads on Windows")
|
||||||
t.Skip("Skipping io.Copy error test - requires file system race condition")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
src, err := os.CreateTemp("", "copy-test-unreadable-*")
|
||||||
func TestCopyCleanupOnError(t *testing.T) {
|
|
||||||
// Create a source file that we'll make unreadable after opening
|
|
||||||
src, err := os.CreateTemp("", "copy-test-cleanup-*")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp file: %v", err)
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(src.Name())
|
defer os.Remove(src.Name())
|
||||||
|
|
||||||
// Write some content
|
if _, err := src.WriteString("test content"); err != nil {
|
||||||
content := "test content for cleanup"
|
|
||||||
if _, err := src.WriteString(content); err != nil {
|
|
||||||
t.Fatalf("failed to write content: %v", err)
|
t.Fatalf("failed to write content: %v", err)
|
||||||
}
|
}
|
||||||
src.Close()
|
src.Close()
|
||||||
@@ -150,38 +125,21 @@ func TestCopyCleanupOnError(t *testing.T) {
|
|||||||
CheatPath: "test",
|
CheatPath: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destination path
|
dest := filepath.Join(os.TempDir(), "copy-unreadable-test.txt")
|
||||||
dest := filepath.Join(os.TempDir(), "copy-cleanup-test.txt")
|
defer os.Remove(dest)
|
||||||
defer os.Remove(dest) // Clean up if test fails
|
|
||||||
|
|
||||||
// Make the source file unreadable (simulating a read error during copy)
|
|
||||||
// This is platform-specific, but should work on Unix-like systems
|
|
||||||
if err := os.Chmod(src.Name(), 0000); err != nil {
|
if err := os.Chmod(src.Name(), 0000); err != nil {
|
||||||
t.Skip("Cannot change file permissions on this platform")
|
t.Skip("Cannot change file permissions on this platform")
|
||||||
}
|
}
|
||||||
defer os.Chmod(src.Name(), 0644) // Restore permissions for cleanup
|
defer os.Chmod(src.Name(), 0644)
|
||||||
|
|
||||||
// Attempt to copy - this should fail during io.Copy
|
|
||||||
err = sheet.Copy(dest)
|
err = sheet.Copy(dest)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected Copy to fail with permission error")
|
t.Error("expected Copy to fail with permission error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the destination file was cleaned up
|
// Destination should not exist since the error occurs before it is created
|
||||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||||
t.Error("Destination file should have been removed after copy failure")
|
t.Error("destination file should not exist after open failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsHelper(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package sheet provides functionality for parsing and managing individual cheat sheets.
|
|
||||||
//
|
|
||||||
// A sheet represents a single cheatsheet file containing helpful commands, notes,
|
|
||||||
// or documentation. Sheets can include optional YAML frontmatter for metadata
|
|
||||||
// such as tags and syntax highlighting preferences.
|
|
||||||
//
|
|
||||||
// # Sheet Format
|
|
||||||
//
|
|
||||||
// Sheets are plain text files that may begin with YAML frontmatter:
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// syntax: bash
|
|
||||||
// tags: [networking, linux, ssh]
|
|
||||||
// ---
|
|
||||||
// # Connect to remote server
|
|
||||||
// ssh user@hostname
|
|
||||||
//
|
|
||||||
// # Copy files over SSH
|
|
||||||
// scp local_file user@hostname:/remote/path
|
|
||||||
//
|
|
||||||
// The frontmatter is optional. If omitted, the sheet will use default values.
|
|
||||||
//
|
|
||||||
// # Core Types
|
|
||||||
//
|
|
||||||
// The Sheet type contains:
|
|
||||||
// - Title: The sheet's name (derived from filename)
|
|
||||||
// - Path: Full filesystem path to the sheet
|
|
||||||
// - Text: The content of the sheet (without frontmatter)
|
|
||||||
// - Tags: Categories assigned to the sheet
|
|
||||||
// - Syntax: Language hint for syntax highlighting
|
|
||||||
// - ReadOnly: Whether the sheet can be modified
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - New: Creates a new Sheet from a file path
|
|
||||||
// - Parse: Extracts frontmatter and content from sheet text
|
|
||||||
// - Search: Searches sheet content using regular expressions
|
|
||||||
// - Colorize: Applies syntax highlighting to sheet content
|
|
||||||
//
|
|
||||||
// # Syntax Highlighting
|
|
||||||
//
|
|
||||||
// The package integrates with the Chroma library to provide syntax highlighting.
|
|
||||||
// Supported languages include bash, python, go, javascript, and many others.
|
|
||||||
// The syntax can be specified in the frontmatter or auto-detected.
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Load a sheet from disk
|
|
||||||
// s, err := sheet.New("/path/to/sheet", []string{"personal"}, false)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Search for content
|
|
||||||
// matches, err := s.Search("ssh", false)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Apply syntax highlighting
|
|
||||||
// colorized, err := s.Colorize(config)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
package sheet
|
|
||||||
@@ -2,7 +2,6 @@ package sheet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -11,9 +10,9 @@ import (
|
|||||||
// Parse parses cheatsheet frontmatter
|
// Parse parses cheatsheet frontmatter
|
||||||
func parse(markdown string) (frontmatter, string, error) {
|
func parse(markdown string) (frontmatter, string, error) {
|
||||||
|
|
||||||
// determine the appropriate line-break for the platform
|
// detect the line-break style used in the content
|
||||||
linebreak := "\n"
|
linebreak := "\n"
|
||||||
if runtime.GOOS == "windows" {
|
if strings.Contains(markdown, "\r\n") {
|
||||||
linebreak = "\r\n"
|
linebreak = "\r\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
||||||
func TestParseWindowsLineEndings(t *testing.T) {
|
func TestParseWindowsLineEndings(t *testing.T) {
|
||||||
// Only test Windows line endings on Windows
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Skipping Windows line ending test on non-Windows platform")
|
|
||||||
}
|
|
||||||
|
|
||||||
// stub our cheatsheet content with Windows line endings
|
// stub our cheatsheet content with Windows line endings
|
||||||
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
||||||
|
|
||||||
@@ -33,22 +27,3 @@ func TestParseWindowsLineEndings(t *testing.T) {
|
|||||||
t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax)
|
t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestParseInvalidYAML tests parsing with invalid YAML in frontmatter
|
|
||||||
func TestParseInvalidYAML(t *testing.T) {
|
|
||||||
// stub our cheatsheet content with invalid YAML
|
|
||||||
markdown := `---
|
|
||||||
syntax: go
|
|
||||||
tags: [ test
|
|
||||||
unclosed bracket
|
|
||||||
---
|
|
||||||
To foo the bar: baz`
|
|
||||||
|
|
||||||
// parse the frontmatter
|
|
||||||
_, _, err := parse(markdown)
|
|
||||||
|
|
||||||
// assert that an error was returned for invalid YAML
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected error for invalid YAML, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ To foo the bar: baz`
|
|||||||
t.Errorf("failed to parse tags: want: %s, got: %s", want, fm.Tags[0])
|
t.Errorf("failed to parse tags: want: %s, got: %s", want, fm.Tags[0])
|
||||||
}
|
}
|
||||||
if len(fm.Tags) != 1 {
|
if len(fm.Tags) != 1 {
|
||||||
t.Errorf("failed to parse tags: want: len 0, got: len %d", len(fm.Tags))
|
t.Errorf("failed to parse tags: want: len 1, got: len %d", len(fm.Tags))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,69 +122,3 @@ func FuzzSearchRegex(f *testing.F) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzSearchCatastrophicBacktracking specifically tests for regex patterns
|
|
||||||
// that could cause performance issues
|
|
||||||
func FuzzSearchCatastrophicBacktracking(f *testing.F) {
|
|
||||||
// Seed with patterns known to potentially cause issues
|
|
||||||
f.Add("a", 10, 5)
|
|
||||||
f.Add("x", 20, 3)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, char string, repeats int, groups int) {
|
|
||||||
// Limit the size to avoid memory issues in the test
|
|
||||||
if repeats > 30 || repeats < 0 || groups > 10 || groups < 0 || len(char) > 5 {
|
|
||||||
t.Skip("Skipping invalid or overly large test case")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct patterns that might cause backtracking
|
|
||||||
patterns := []string{
|
|
||||||
strings.Repeat(char, repeats),
|
|
||||||
"(" + char + "+)+",
|
|
||||||
"(" + char + "*)*",
|
|
||||||
"(" + char + "|" + char + ")*",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add nested groups
|
|
||||||
if groups > 0 && groups < 10 {
|
|
||||||
nested := char
|
|
||||||
for i := 0; i < groups; i++ {
|
|
||||||
nested = "(" + nested + ")+"
|
|
||||||
}
|
|
||||||
patterns = append(patterns, nested)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test text that might trigger backtracking
|
|
||||||
testText := strings.Repeat(char, repeats) + "x"
|
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
// Try to compile the pattern
|
|
||||||
reg, err := regexp.Compile(pattern)
|
|
||||||
if err != nil {
|
|
||||||
// Invalid pattern, skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with timeout
|
|
||||||
done := make(chan bool, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Errorf("Search panicked with backtracking pattern %q: %v", pattern, r)
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
sheet := Sheet{Text: testText}
|
|
||||||
_ = sheet.Search(reg)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
// Completed successfully
|
|
||||||
case <-time.After(50 * time.Millisecond):
|
|
||||||
t.Logf("Warning: potential backtracking issue with pattern %q (completed slowly)", pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSheetSuccess asserts that sheets initialize properly
|
// TestSheetSuccess asserts that sheets initialize properly
|
||||||
@@ -14,7 +14,7 @@ func TestSheetSuccess(t *testing.T) {
|
|||||||
sheet, err := New(
|
sheet, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("sheet/foo"),
|
mocks.Path("sheet/foo"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -27,10 +27,10 @@ func TestSheetSuccess(t *testing.T) {
|
|||||||
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
|
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sheet.Path != mock.Path("sheet/foo") {
|
if sheet.Path != mocks.Path("sheet/foo") {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"failed to init path: want: %s, got: %s",
|
"failed to init path: want: %s, got: %s",
|
||||||
mock.Path("sheet/foo"),
|
mocks.Path("sheet/foo"),
|
||||||
sheet.Path,
|
sheet.Path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func TestSheetFailure(t *testing.T) {
|
|||||||
_, err := New(
|
_, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("/does-not-exist"),
|
mocks.Path("/does-not-exist"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -80,7 +80,7 @@ func TestSheetFrontMatterFailure(t *testing.T) {
|
|||||||
_, err := New(
|
_, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("sheet/bad-fm"),
|
mocks.Path("sheet/bad-fm"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// Tagged returns true if a sheet was tagged with `needle`
|
// Tagged returns true if a sheet was tagged with `needle`
|
||||||
func (s *Sheet) Tagged(needle string) bool {
|
func (s *Sheet) Tagged(needle string) bool {
|
||||||
|
return slices.Contains(s.Tags, needle)
|
||||||
// if any of the tags match `needle`, return `true`
|
|
||||||
for _, tag := range s.Tags {
|
|
||||||
if tag == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, return `false`
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
internal/sheet/validate.go
Normal file
40
internal/sheet/validate.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package sheet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate ensures that a cheatsheet name does not contain
|
||||||
|
// directory traversal sequences or other potentially dangerous patterns.
|
||||||
|
func Validate(name string) error {
|
||||||
|
// Reject empty names
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject names containing directory traversal
|
||||||
|
if strings.Contains(name, "..") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject absolute paths
|
||||||
|
if filepath.IsAbs(name) {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot be an absolute path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject names that start with ~ (home directory expansion)
|
||||||
|
if strings.HasPrefix(name, "~") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot start with '~'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject hidden files (files that start with a dot)
|
||||||
|
// We don't display hidden files, so we shouldn't create them
|
||||||
|
filename := filepath.Base(name)
|
||||||
|
if strings.HasPrefix(filename, ".") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cheatpath
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FuzzValidateSheetName tests the ValidateSheetName function with fuzzing
|
// FuzzValidate tests the Validate function with fuzzing
|
||||||
// to ensure it properly prevents path traversal and other security issues
|
// to ensure it properly prevents path traversal and other security issues
|
||||||
func FuzzValidateSheetName(f *testing.F) {
|
func FuzzValidate(f *testing.F) {
|
||||||
// Add seed corpus with various valid and malicious inputs
|
// Add seed corpus with various valid and malicious inputs
|
||||||
// Valid names
|
// Valid names
|
||||||
f.Add("docker")
|
f.Add("docker")
|
||||||
@@ -84,11 +84,11 @@ func FuzzValidateSheetName(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("ValidateSheetName panicked with input %q: %v", input, r)
|
t.Errorf("Validate panicked with input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := ValidateSheetName(input)
|
err := Validate(input)
|
||||||
|
|
||||||
// Security invariants that must always hold
|
// Security invariants that must always hold
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -129,8 +129,8 @@ func FuzzValidateSheetName(f *testing.F) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzValidateSheetNamePathTraversal specifically targets path traversal bypasses
|
// FuzzValidatePathTraversal specifically targets path traversal bypasses
|
||||||
func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
func FuzzValidatePathTraversal(f *testing.F) {
|
||||||
// Seed corpus focusing on path traversal variations
|
// Seed corpus focusing on path traversal variations
|
||||||
f.Add("..", "/", "")
|
f.Add("..", "/", "")
|
||||||
f.Add("", "..", "/")
|
f.Add("", "..", "/")
|
||||||
@@ -153,11 +153,11 @@ func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("ValidateSheetName panicked with constructed input %q: %v", input, r)
|
t.Errorf("Validate panicked with constructed input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := ValidateSheetName(input)
|
err := Validate(input)
|
||||||
|
|
||||||
// If the input contains literal "..", it must be rejected
|
// If the input contains literal "..", it must be rejected
|
||||||
if strings.Contains(input, "..") && err == nil {
|
if strings.Contains(input, "..") && err == nil {
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package cheatpath
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateSheetName(t *testing.T) {
|
func TestValidate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
@@ -53,9 +54,15 @@ func TestValidateSheetName(t *testing.T) {
|
|||||||
errMsg: "'..'",
|
errMsg: "'..'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "absolute path",
|
name: "absolute path unix",
|
||||||
input: "/etc/passwd",
|
input: "/etc/passwd",
|
||||||
wantErr: true,
|
wantErr: runtime.GOOS != "windows", // /etc/passwd is not absolute on Windows
|
||||||
|
errMsg: "absolute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absolute path windows",
|
||||||
|
input: `C:\evil`,
|
||||||
|
wantErr: runtime.GOOS == "windows", // C:\evil is not absolute on Unix
|
||||||
errMsg: "absolute",
|
errMsg: "absolute",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -91,14 +98,14 @@ func TestValidateSheetName(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := ValidateSheetName(tt.input)
|
err := Validate(tt.input)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("ValidateName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil && tt.errMsg != "" {
|
if err != nil && tt.errMsg != "" {
|
||||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||||
t.Errorf("ValidateName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
t.Errorf("Validate(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package sheets manages collections of cheat sheets across multiple cheatpaths.
|
|
||||||
//
|
|
||||||
// The sheets package provides functionality to:
|
|
||||||
// - Load sheets from multiple cheatpaths
|
|
||||||
// - Consolidate duplicate sheets (with precedence rules)
|
|
||||||
// - Filter sheets by tags
|
|
||||||
// - Sort sheets alphabetically
|
|
||||||
// - Extract unique tags across all sheets
|
|
||||||
//
|
|
||||||
// # Loading Sheets
|
|
||||||
//
|
|
||||||
// Sheets are loaded recursively from cheatpath directories, excluding:
|
|
||||||
// - Hidden files (starting with .)
|
|
||||||
// - Files in .git directories
|
|
||||||
// - Files with extensions (sheets have no extension)
|
|
||||||
//
|
|
||||||
// # Consolidation
|
|
||||||
//
|
|
||||||
// When multiple cheatpaths contain sheets with the same name, consolidation
|
|
||||||
// rules apply based on the order of cheatpaths. Sheets from earlier paths
|
|
||||||
// override those from later paths, allowing personal sheets to override
|
|
||||||
// community sheets.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// cheatpaths:
|
|
||||||
// 1. personal: ~/cheat
|
|
||||||
// 2. community: ~/cheat/community
|
|
||||||
//
|
|
||||||
// If both contain "git", the version from "personal" is used.
|
|
||||||
//
|
|
||||||
// # Filtering
|
|
||||||
//
|
|
||||||
// Sheets can be filtered by:
|
|
||||||
// - Tags: Include only sheets with specific tags
|
|
||||||
// - Cheatpath: Include only sheets from specific paths
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - Load: Loads all sheets from the given cheatpaths
|
|
||||||
// - Filter: Filters sheets by tag
|
|
||||||
// - Consolidate: Merges sheets from multiple paths with precedence
|
|
||||||
// - Sort: Sorts sheets alphabetically by title
|
|
||||||
// - Tags: Extracts all unique tags from sheets
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Load sheets from all cheatpaths
|
|
||||||
// allSheets, err := sheets.Load(cheatpaths)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Consolidate to handle duplicates
|
|
||||||
// consolidated := sheets.Consolidate(allSheets)
|
|
||||||
//
|
|
||||||
// // Filter by tag
|
|
||||||
// filtered := sheets.Filter(consolidated, "networking")
|
|
||||||
//
|
|
||||||
// // Sort alphabetically
|
|
||||||
// sheets.Sort(filtered)
|
|
||||||
//
|
|
||||||
// // Get all unique tags
|
|
||||||
// tags := sheets.Tags(consolidated)
|
|
||||||
package sheets
|
|
||||||
@@ -18,28 +18,26 @@ func TestFilterSingleTag(t *testing.T) {
|
|||||||
|
|
||||||
map[string]sheet.Sheet{
|
map[string]sheet.Sheet{
|
||||||
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
|
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
|
||||||
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
|
"bar": sheet.Sheet{Title: "bar", Tags: []string{"charlie"}},
|
||||||
},
|
},
|
||||||
|
|
||||||
map[string]sheet.Sheet{
|
map[string]sheet.Sheet{
|
||||||
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
|
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha"}},
|
||||||
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
|
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the cheatsheets
|
// filter the cheatsheets
|
||||||
filtered := Filter(cheatpaths, []string{"bravo"})
|
filtered := Filter(cheatpaths, []string{"alpha"})
|
||||||
|
|
||||||
// assert that the expect results were returned
|
// assert that the expect results were returned
|
||||||
want := []map[string]sheet.Sheet{
|
want := []map[string]sheet.Sheet{
|
||||||
map[string]sheet.Sheet{
|
map[string]sheet.Sheet{
|
||||||
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
|
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
|
||||||
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
map[string]sheet.Sheet{
|
map[string]sheet.Sheet{
|
||||||
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
|
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha"}},
|
||||||
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/repo"
|
|
||||||
"github.com/cheat/cheat/internal/sheet"
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load produces a map of cheatsheet titles to filesystem paths
|
// Load produces a map of cheatsheet titles to filesystem paths
|
||||||
func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
func Load(cheatpaths []cp.Path) ([]map[string]sheet.Sheet, error) {
|
||||||
|
|
||||||
// create a slice of maps of sheets. This structure will store all sheets
|
// create a slice of maps of sheets. This structure will store all sheets
|
||||||
// that are associated with each cheatpath.
|
// that are associated with each cheatpath.
|
||||||
@@ -27,10 +26,10 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
|
|
||||||
// recursively iterate over the cheatpath, and load each cheatsheet
|
// recursively iterate over the cheatpath, and load each cheatsheet
|
||||||
// encountered along the way
|
// encountered along the way
|
||||||
err := filepath.Walk(
|
err := filepath.WalkDir(
|
||||||
cheatpath.Path, func(
|
cheatpath.Path, func(
|
||||||
path string,
|
path string,
|
||||||
info os.FileInfo,
|
d fs.DirEntry,
|
||||||
err error) error {
|
err error) error {
|
||||||
|
|
||||||
// fail if an error occurred while walking the directory
|
// fail if an error occurred while walking the directory
|
||||||
@@ -38,8 +37,12 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
return fmt.Errorf("failed to walk path: %v", err)
|
return fmt.Errorf("failed to walk path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't register directories as cheatsheets
|
if d.IsDir() {
|
||||||
if info.IsDir() {
|
// skip .git directories to avoid hundreds/thousands of
|
||||||
|
// needless syscalls (see repo.GitDir for full history)
|
||||||
|
if filepath.Base(path) == ".git" {
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,17 +66,6 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
string(os.PathSeparator),
|
string(os.PathSeparator),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Don't walk the `.git` directory. Doing so creates
|
|
||||||
// hundreds/thousands of needless syscalls and could
|
|
||||||
// potentially harm performance on machines with slow disks.
|
|
||||||
skip, err := repo.GitDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to identify .git directory: %v", err)
|
|
||||||
}
|
|
||||||
if skip {
|
|
||||||
return fs.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the cheatsheet file into a `sheet` struct
|
// parse the cheatsheet file into a `sheet` struct
|
||||||
s, err := sheet.New(
|
s, err := sheet.New(
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLoad asserts that sheets on valid cheatpaths can be loaded successfully
|
// TestLoad asserts that sheets on valid cheatpaths can be loaded successfully
|
||||||
func TestLoad(t *testing.T) {
|
func TestLoad(t *testing.T) {
|
||||||
|
|
||||||
// mock cheatpaths
|
// mock cheatpaths
|
||||||
cheatpaths := []cheatpath.Cheatpath{
|
cheatpaths := []cheatpath.Path{
|
||||||
{
|
{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: path.Join(mock.Path("cheatsheets"), "community"),
|
Path: path.Join(mocks.Path("cheatsheets"), "community"),
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: path.Join(mock.Path("cheatsheets"), "personal"),
|
Path: path.Join(mocks.Path("cheatsheets"), "personal"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ func TestLoad(t *testing.T) {
|
|||||||
func TestLoadBadPath(t *testing.T) {
|
func TestLoadBadPath(t *testing.T) {
|
||||||
|
|
||||||
// mock a bad cheatpath
|
// mock a bad cheatpath
|
||||||
cheatpaths := []cheatpath.Cheatpath{
|
cheatpaths := []cheatpath.Path{
|
||||||
{
|
{
|
||||||
Name: "badpath",
|
Name: "badpath",
|
||||||
Path: "/cheat/test/path/does/not/exist",
|
Path: "/cheat/test/path/does/not/exist",
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ func Tags(cheatpaths []map[string]sheet.Sheet) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort the slice
|
// sort the slice
|
||||||
sort.Slice(sorted, func(i, j int) bool {
|
sort.Strings(sorted)
|
||||||
return sorted[i] < sorted[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,64 +127,3 @@ func FuzzTags(f *testing.F) {
|
|||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzTagsStress tests Tags function with large numbers of tags
|
|
||||||
func FuzzTagsStress(f *testing.F) {
|
|
||||||
// Seed: number of unique tags, number of sheets, tags per sheet
|
|
||||||
f.Add(10, 10, 5)
|
|
||||||
f.Add(100, 50, 10)
|
|
||||||
f.Add(1000, 100, 20)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, numUniqueTags int, numSheets int, tagsPerSheet int) {
|
|
||||||
// Limit to reasonable values
|
|
||||||
if numUniqueTags > 1000 || numUniqueTags < 0 ||
|
|
||||||
numSheets > 1000 || numSheets < 0 ||
|
|
||||||
tagsPerSheet > 100 || tagsPerSheet < 0 {
|
|
||||||
t.Skip("Skipping unreasonable test case")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique tags
|
|
||||||
uniqueTags := make([]string, numUniqueTags)
|
|
||||||
for i := 0; i < numUniqueTags; i++ {
|
|
||||||
uniqueTags[i] = "tag" + string(rune(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create sheets with random tags
|
|
||||||
cheatpaths := []map[string]sheet.Sheet{
|
|
||||||
make(map[string]sheet.Sheet),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numSheets; i++ {
|
|
||||||
// Select random tags for this sheet
|
|
||||||
sheetTags := make([]string, 0, tagsPerSheet)
|
|
||||||
for j := 0; j < tagsPerSheet && j < numUniqueTags; j++ {
|
|
||||||
// Distribute tags across sheets
|
|
||||||
tagIndex := (i*tagsPerSheet + j) % numUniqueTags
|
|
||||||
sheetTags = append(sheetTags, uniqueTags[tagIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
cheatpaths[0]["sheet"+string(rune(i))] = sheet.Sheet{
|
|
||||||
Title: "sheet" + string(rune(i)),
|
|
||||||
Tags: sheetTags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should handle large numbers efficiently
|
|
||||||
func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Errorf("Tags panicked with %d unique tags, %d sheets, %d tags/sheet: %v",
|
|
||||||
numUniqueTags, numSheets, tagsPerSheet, r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := Tags(cheatpaths)
|
|
||||||
|
|
||||||
// Should have at most numUniqueTags in result
|
|
||||||
if len(result) > numUniqueTags {
|
|
||||||
t.Errorf("More tags in result (%d) than unique tags created (%d)",
|
|
||||||
len(result), numUniqueTags)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
23
mocks/path.go
Normal file
23
mocks/path.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Package mocks provides test fixture data and helpers for unit tests.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path returns the absolute path to the specified mock file within
|
||||||
|
// the mocks/ directory.
|
||||||
|
func Path(filename string) string {
|
||||||
|
_, thisfile, _, _ := runtime.Caller(0)
|
||||||
|
|
||||||
|
file, err := filepath.Abs(
|
||||||
|
filepath.Join(filepath.Dir(thisfile), filename),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
@@ -16,12 +16,12 @@ DURATION="${1:-15s}"
|
|||||||
# Define fuzz tests: "TestName:Package:Description"
|
# Define fuzz tests: "TestName:Package:Description"
|
||||||
TESTS=(
|
TESTS=(
|
||||||
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
||||||
"FuzzValidateSheetName:./internal/cheatpath:sheet name validation (path traversal protection)"
|
"FuzzValidate:./internal/sheet:sheet name validation (path traversal protection)"
|
||||||
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
||||||
"FuzzSearchCatastrophicBacktracking:./internal/sheet:catastrophic backtracking"
|
|
||||||
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
||||||
"FuzzFilter:./internal/sheets:tag filtering operations"
|
"FuzzFilter:./internal/sheets:tag filtering operations"
|
||||||
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
||||||
|
"FuzzFindLocalCheatpath:./internal/config:recursive .cheat directory discovery"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "Running fuzz tests ($DURATION each)..."
|
echo "Running fuzz tests ($DURATION each)..."
|
||||||
129
test/integration/brief_integration_test.go
Normal file
129
test/integration/brief_integration_test.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBriefFlagIntegration exercises the -b/--brief flag end-to-end.
|
||||||
|
func TestBriefFlagIntegration(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env vars")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the cheat binary once for all sub-tests.
|
||||||
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a temp environment with some cheatsheets.
|
||||||
|
root := t.TempDir()
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(sheetsDir, "tar"),
|
||||||
|
[]byte("---\nsyntax: bash\ntags: [ compression ]\n---\ntar xf archive.tar\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(sheetsDir, "curl"),
|
||||||
|
[]byte("---\nsyntax: bash\ntags: [ networking, http ]\n---\ncurl https://example.com\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := filepath.Join(root, "conf.yml")
|
||||||
|
conf := fmt.Sprintf("---\neditor: vi\ncolorize: false\ncheatpaths:\n - name: test\n path: %s\n readonly: true\n", sheetsDir)
|
||||||
|
os.WriteFile(confPath, []byte(conf), 0644)
|
||||||
|
|
||||||
|
env := []string{
|
||||||
|
"CHEAT_CONFIG_PATH=" + confPath,
|
||||||
|
"HOME=" + root,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"EDITOR=vi",
|
||||||
|
}
|
||||||
|
|
||||||
|
run := func(t *testing.T, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
cmd := exec.Command(binPath, args...)
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat %v failed: %v\nOutput: %s", args, err, output)
|
||||||
|
}
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("brief output omits file path column", func(t *testing.T) {
|
||||||
|
output := run(t, "-b")
|
||||||
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
|
||||||
|
// Header should have title and tags but not file
|
||||||
|
if !strings.Contains(lines[0], "title:") {
|
||||||
|
t.Errorf("expected title: in header, got: %s", lines[0])
|
||||||
|
}
|
||||||
|
if !strings.Contains(lines[0], "tags:") {
|
||||||
|
t.Errorf("expected tags: in header, got: %s", lines[0])
|
||||||
|
}
|
||||||
|
if strings.Contains(lines[0], "file:") {
|
||||||
|
t.Errorf("brief output should not contain file: column, got: %s", lines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data lines should not contain the sheets directory path
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
if strings.Contains(line, sheetsDir) {
|
||||||
|
t.Errorf("brief output should not contain file paths, got: %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list output still includes file path column", func(t *testing.T) {
|
||||||
|
output := run(t, "-l")
|
||||||
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
|
||||||
|
if !strings.Contains(lines[0], "file:") {
|
||||||
|
t.Errorf("list output should contain file: column, got: %s", lines[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("brief with filter works", func(t *testing.T) {
|
||||||
|
output := run(t, "-b", "tar")
|
||||||
|
if !strings.Contains(output, "tar") {
|
||||||
|
t.Errorf("expected tar in output, got: %s", output)
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "curl") {
|
||||||
|
t.Errorf("filter should exclude curl, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("combined -lb works identically to -b", func(t *testing.T) {
|
||||||
|
briefOnly := run(t, "-b", "tar")
|
||||||
|
combined := run(t, "-lb", "tar")
|
||||||
|
if briefOnly != combined {
|
||||||
|
t.Errorf("-b and -lb should produce identical output\n-b:\n%s\n-lb:\n%s", briefOnly, combined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("brief with tag filter works", func(t *testing.T) {
|
||||||
|
output := run(t, "-b", "-t", "networking")
|
||||||
|
if !strings.Contains(output, "curl") {
|
||||||
|
t.Errorf("expected curl in tag-filtered output, got: %s", output)
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "tar") {
|
||||||
|
// tar is tagged "compression", not "networking"
|
||||||
|
t.Errorf("tag filter should exclude tar, got: %s", output)
|
||||||
|
}
|
||||||
|
if strings.Contains(output, "file:") {
|
||||||
|
t.Errorf("brief output should not contain file: column, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
246
test/integration/cheatpath_integration_test.go
Normal file
246
test/integration/cheatpath_integration_test.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hasCwdCheatpath checks whether the --directories output contains a
|
||||||
|
// cheatpath named "cwd". The output format is "name: path\n" per line
|
||||||
|
// (tabwriter-aligned), so we look for a line beginning with "cwd".
|
||||||
|
func hasCwdCheatpath(output string) bool {
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
if strings.HasPrefix(line, "cwd") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalCheatpathIntegration exercises the recursive .cheat directory
|
||||||
|
// discovery end-to-end: it builds the real cheat binary, sets up filesystem
|
||||||
|
// layouts, and verifies behaviour from the user's perspective.
|
||||||
|
func TestLocalCheatpathIntegration(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env vars")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the cheat binary once for all sub-tests.
|
||||||
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cheatEnv returns a minimal environment for the cheat binary.
|
||||||
|
cheatEnv := func(confPath, home string) []string {
|
||||||
|
return []string{
|
||||||
|
"CHEAT_CONFIG_PATH=" + confPath,
|
||||||
|
"HOME=" + home,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"EDITOR=vi",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeConfig writes a minimal valid config file referencing sheetsDir.
|
||||||
|
writeConfig := func(t *testing.T, dir, sheetsDir string) string {
|
||||||
|
t.Helper()
|
||||||
|
conf := fmt.Sprintf("---\neditor: vi\ncolorize: false\ncheatpaths:\n - name: base\n path: %s\n readonly: true\n", sheetsDir)
|
||||||
|
confPath := filepath.Join(dir, "conf.yml")
|
||||||
|
if err := os.WriteFile(confPath, []byte(conf), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
return confPath
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("parent .cheat is discovered from subdirectory", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
// Configured cheatpath (empty but must exist for validation)
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
// .cheat at root with a cheatsheet
|
||||||
|
dotCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(dotCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(dotCheat, "localsheet"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\necho hello from local\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
// Work from a subdirectory
|
||||||
|
workDir := filepath.Join(root, "src", "pkg")
|
||||||
|
os.MkdirAll(workDir, 0755)
|
||||||
|
env := cheatEnv(confPath, root)
|
||||||
|
|
||||||
|
// --directories should list "cwd" cheatpath
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("expected 'cwd' cheatpath in --directories output:\n%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewing the cheatsheet should show its content
|
||||||
|
cmd2 := exec.Command(binPath, "localsheet")
|
||||||
|
cmd2.Dir = workDir
|
||||||
|
cmd2.Env = env
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat localsheet failed: %v\nOutput: %s", err, output2)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output2), "echo hello from local") {
|
||||||
|
t.Errorf("expected cheatsheet content, got:\n%s", output2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("grandparent .cheat is discovered from deep subdirectory", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
dotCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(dotCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(dotCheat, "deepsheet"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\ndeep discovery works\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
deepDir := filepath.Join(root, "a", "b", "c", "d", "e")
|
||||||
|
os.MkdirAll(deepDir, 0755)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath, "deepsheet")
|
||||||
|
cmd.Dir = deepDir
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat deepsheet failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "deep discovery works") {
|
||||||
|
t.Errorf("expected cheatsheet content, got:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nearest .cheat wins over ancestor .cheat", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
// .cheat at root
|
||||||
|
rootCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(rootCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(rootCheat, "shared"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nfrom root\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
// .cheat at project/ (nearer)
|
||||||
|
projectDir := filepath.Join(root, "project")
|
||||||
|
os.MkdirAll(projectDir, 0755)
|
||||||
|
projectCheat := filepath.Join(projectDir, ".cheat")
|
||||||
|
os.Mkdir(projectCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(projectCheat, "shared"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nfrom project nearest\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
workDir := filepath.Join(projectDir, "src")
|
||||||
|
os.MkdirAll(workDir, 0755)
|
||||||
|
env := cheatEnv(confPath, root)
|
||||||
|
|
||||||
|
// --directories should list the nearer cheatpath
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), projectCheat) {
|
||||||
|
t.Errorf("expected project .cheat path in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "shared" sheet should come from the nearer .cheat
|
||||||
|
cmd2 := exec.Command(binPath, "shared")
|
||||||
|
cmd2.Dir = workDir
|
||||||
|
cmd2.Env = env
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat shared failed: %v\nOutput: %s", err, output2)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output2), "from project nearest") {
|
||||||
|
t.Errorf("expected nearest .cheat content, got:\n%s", output2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no .cheat directory means no cwd cheatpath", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
// Need at least one sheet for --directories to work without error
|
||||||
|
os.WriteFile(filepath.Join(sheetsDir, "placeholder"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nplaceholder\n"), 0644)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
// No .cheat anywhere under root
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("'cwd' cheatpath should not appear when no .cheat exists:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(".cheat file (not directory) is ignored", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(sheetsDir, "placeholder"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nplaceholder\n"), 0644)
|
||||||
|
|
||||||
|
// Create .cheat as a regular file
|
||||||
|
os.WriteFile(filepath.Join(root, ".cheat"), []byte("not a dir"), 0644)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("'cwd' should not appear for a .cheat file:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
305
test/integration/first_run_integration_test.go
Normal file
305
test/integration/first_run_integration_test.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFirstRunIntegration exercises the end-to-end first-run experience:
|
||||||
|
// no config exists, the binary creates one, and subsequent runs succeed.
|
||||||
|
// This is the regression test for issues #721, #771, and #730.
|
||||||
|
func TestFirstRunIntegration(t *testing.T) {
|
||||||
|
// Build the cheat binary
|
||||||
|
binName := "cheat_test"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binName += ".exe"
|
||||||
|
}
|
||||||
|
binPath := filepath.Join(t.TempDir(), binName)
|
||||||
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("init comments out community", func(t *testing.T) {
|
||||||
|
testHome := t.TempDir()
|
||||||
|
env := firstRunEnv(testHome)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath, "--init")
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("--init failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
outStr := string(output)
|
||||||
|
|
||||||
|
// No placeholder strings should survive (regression for #721)
|
||||||
|
assertNoPlaceholders(t, outStr)
|
||||||
|
|
||||||
|
// Community cheatpath should be commented out
|
||||||
|
assertCommunityCommentedOut(t, outStr)
|
||||||
|
|
||||||
|
// Personal and work cheatpaths should be active (uncommented)
|
||||||
|
assertCheatpathActive(t, outStr, "personal")
|
||||||
|
assertCheatpathActive(t, outStr, "work")
|
||||||
|
|
||||||
|
// Should include clone instructions
|
||||||
|
if !strings.Contains(outStr, "git clone") {
|
||||||
|
t.Error("expected git clone instructions in --init output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the config and verify it loads without errors.
|
||||||
|
// --init only outputs config, it doesn't create directories,
|
||||||
|
// so we need to create the cheatpath dirs the config references.
|
||||||
|
confpath := filepath.Join(testHome, "conf.yml")
|
||||||
|
if err := os.WriteFile(confpath, output, 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the confdir that --init used (same logic as cmd_init.go)
|
||||||
|
initConfpaths := firstRunConfpaths(testHome)
|
||||||
|
initConfdir := filepath.Dir(initConfpaths[0])
|
||||||
|
for _, name := range []string{"personal", "work"} {
|
||||||
|
dir := filepath.Join(initConfdir, "cheatsheets", name)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create %s dir: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd2 := exec.Command(binPath, "--directories")
|
||||||
|
cmd2.Env = append(append([]string{}, env...), "CHEAT_CONFIG_PATH="+confpath)
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("config from --init failed to load: %v\nOutput: %s", err, output2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("decline config creation", func(t *testing.T) {
|
||||||
|
testHome := t.TempDir()
|
||||||
|
env := firstRunEnv(testHome)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath)
|
||||||
|
cmd.Env = env
|
||||||
|
cmd.Stdin = strings.NewReader("n\n")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat exited with error: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no config was created
|
||||||
|
if firstRunConfigExists(testHome) {
|
||||||
|
t.Error("config file was created despite user declining")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("accept config decline community", func(t *testing.T) {
|
||||||
|
testHome := t.TempDir()
|
||||||
|
env := firstRunEnv(testHome)
|
||||||
|
|
||||||
|
// First run: yes to create config, no to community cheatsheets
|
||||||
|
cmd := exec.Command(binPath)
|
||||||
|
cmd.Env = env
|
||||||
|
cmd.Stdin = strings.NewReader("y\nn\n")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first run failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
outStr := string(output)
|
||||||
|
|
||||||
|
// Parse the config path from output
|
||||||
|
confpath := parseCreatedConfPath(t, outStr)
|
||||||
|
if confpath == "" {
|
||||||
|
t.Fatalf("could not find config path in output:\n%s", outStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify config file exists
|
||||||
|
if _, err := os.Stat(confpath); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("config file not found at %s", confpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify config file contents
|
||||||
|
content, err := os.ReadFile(confpath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read config: %v", err)
|
||||||
|
}
|
||||||
|
contentStr := string(content)
|
||||||
|
|
||||||
|
// No placeholder strings should survive (regression for #721)
|
||||||
|
assertNoPlaceholders(t, contentStr)
|
||||||
|
|
||||||
|
// Community cheatpath should be commented out
|
||||||
|
assertCommunityCommentedOut(t, contentStr)
|
||||||
|
|
||||||
|
// Personal and work cheatpaths should be active (uncommented)
|
||||||
|
assertCheatpathActive(t, contentStr, "personal")
|
||||||
|
assertCheatpathActive(t, contentStr, "work")
|
||||||
|
|
||||||
|
// Verify personal and work directories were created
|
||||||
|
confdir := filepath.Dir(confpath)
|
||||||
|
for _, name := range []string{"personal", "work"} {
|
||||||
|
dir := filepath.Join(confdir, "cheatsheets", name)
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected %s directory at %s", name, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Community directory should NOT exist
|
||||||
|
communityDir := filepath.Join(confdir, "cheatsheets", "community")
|
||||||
|
if _, err := os.Stat(communityDir); err == nil {
|
||||||
|
t.Error("community directory should not exist when declined")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Second run: verify the config loads successfully ---
|
||||||
|
// This is the core regression test for #721/#771/#730:
|
||||||
|
// previously, the second run would fail because config.New()
|
||||||
|
// hard-errored on the missing community cheatpath directory.
|
||||||
|
// Use --directories (not --list, which exits 2 when no sheets exist).
|
||||||
|
cmd2 := exec.Command(binPath, "--directories")
|
||||||
|
cmd2.Env = append(append([]string{}, env...), "CHEAT_CONFIG_PATH="+confpath)
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"second run failed (regression for #721/#771/#730): %v\nOutput: %s",
|
||||||
|
err, output2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the output lists the expected cheatpaths
|
||||||
|
outStr2 := string(output2)
|
||||||
|
if !strings.Contains(outStr2, "personal") {
|
||||||
|
t.Errorf("expected 'personal' cheatpath in --directories output:\n%s", outStr2)
|
||||||
|
}
|
||||||
|
if !strings.Contains(outStr2, "work") {
|
||||||
|
t.Errorf("expected 'work' cheatpath in --directories output:\n%s", outStr2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstRunEnv returns a minimal environment for a clean first-run test.
|
||||||
|
func firstRunEnv(home string) []string {
|
||||||
|
env := []string{
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
env = append(env,
|
||||||
|
"APPDATA="+filepath.Join(home, "AppData", "Roaming"),
|
||||||
|
"USERPROFILE="+home,
|
||||||
|
"SystemRoot="+os.Getenv("SystemRoot"),
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
env = append(env,
|
||||||
|
"HOME="+home,
|
||||||
|
"EDITOR=vi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCreatedConfPath extracts the config file path from the installer's
|
||||||
|
// "Created config file: <path>" output. The message may appear mid-line
|
||||||
|
// (after prompt text), so we search for the substring anywhere in the output.
|
||||||
|
func parseCreatedConfPath(t *testing.T, output string) string {
|
||||||
|
t.Helper()
|
||||||
|
const marker = "Created config file: "
|
||||||
|
idx := strings.Index(output, marker)
|
||||||
|
if idx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest := output[idx+len(marker):]
|
||||||
|
// the path ends at the next newline
|
||||||
|
if nl := strings.IndexByte(rest, '\n'); nl >= 0 {
|
||||||
|
rest = rest[:nl]
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(rest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstRunConfpaths returns the config file paths that cheat would check
|
||||||
|
// for the given home directory, matching the logic in config.Paths().
|
||||||
|
func firstRunConfpaths(home string) []string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return []string{
|
||||||
|
filepath.Join(home, "AppData", "Roaming", "cheat", "conf.yml"),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return []string{
|
||||||
|
filepath.Join(home, ".config", "cheat", "conf.yml"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertNoPlaceholders verifies that no template placeholder strings survived
|
||||||
|
// in the config output. This is the regression check for #721 (literal
|
||||||
|
// PAGER_PATH appearing in the config).
|
||||||
|
func assertNoPlaceholders(t *testing.T, content string) {
|
||||||
|
t.Helper()
|
||||||
|
placeholders := []string{
|
||||||
|
"PAGER_PATH",
|
||||||
|
"COMMUNITY_PATH",
|
||||||
|
"PERSONAL_PATH",
|
||||||
|
"WORK_PATH",
|
||||||
|
}
|
||||||
|
for _, p := range placeholders {
|
||||||
|
if strings.Contains(content, p) {
|
||||||
|
t.Errorf("placeholder %q was not replaced in config", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// EDITOR_PATH is special: it survives if no editor is found.
|
||||||
|
// In our test env EDITOR=vi is set, so it should be replaced.
|
||||||
|
if strings.Contains(content, "editor: EDITOR_PATH") {
|
||||||
|
t.Error("placeholder EDITOR_PATH was not replaced in config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertCommunityCommentedOut verifies that the community cheatpath entry
|
||||||
|
// is commented out (not active) in the config.
|
||||||
|
func assertCommunityCommentedOut(t *testing.T, content string) {
|
||||||
|
t.Helper()
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "- name: community" {
|
||||||
|
t.Error("community cheatpath should be commented out")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "#- name: community") {
|
||||||
|
t.Error("expected commented-out community cheatpath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertCheatpathActive verifies that a named cheatpath is present and
|
||||||
|
// uncommented in the config.
|
||||||
|
func assertCheatpathActive(t *testing.T, content string, name string) {
|
||||||
|
t.Helper()
|
||||||
|
marker := "- name: " + name
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == marker {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Errorf("expected active (uncommented) cheatpath %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstRunConfigExists checks whether a cheat config file exists under the
|
||||||
|
// given home directory at any of the standard locations.
|
||||||
|
func firstRunConfigExists(home string) bool {
|
||||||
|
candidates := []string{
|
||||||
|
filepath.Join(home, ".config", "cheat", "conf.yml"),
|
||||||
|
filepath.Join(home, ".cheat", "conf.yml"),
|
||||||
|
filepath.Join(home, "AppData", "Roaming", "cheat", "conf.yml"),
|
||||||
|
}
|
||||||
|
for _, p := range candidates {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
30
test/integration/helpers_test.go
Normal file
30
test/integration/helpers_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// repoRoot returns the absolute path to the repository root.
|
||||||
|
// It derives this from the known location of this source file
|
||||||
|
// (test/integration/) relative to the repo root.
|
||||||
|
func repoRoot(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to determine repo root via runtime.Caller")
|
||||||
|
}
|
||||||
|
// file is <repo>/test/integration/helpers_test.go → go up two dirs
|
||||||
|
return filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoRootBench is the same as repoRoot but for use in benchmarks.
|
||||||
|
func repoRootBench(b *testing.B) string {
|
||||||
|
b.Helper()
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
b.Fatal("failed to determine repo root via runtime.Caller")
|
||||||
|
}
|
||||||
|
return filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -12,9 +13,15 @@ import (
|
|||||||
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
||||||
// path traversal attempts when invoked as a subprocess.
|
// path traversal attempts when invoked as a subprocess.
|
||||||
func TestPathTraversalIntegration(t *testing.T) {
|
func TestPathTraversalIntegration(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env and tools")
|
||||||
|
}
|
||||||
|
|
||||||
// Build the cheat binary
|
// Build the cheat binary
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +153,17 @@ cheatpaths:
|
|||||||
|
|
||||||
// TestPathTraversalRealWorld tests with more realistic scenarios
|
// TestPathTraversalRealWorld tests with more realistic scenarios
|
||||||
func TestPathTraversalRealWorld(t *testing.T) {
|
func TestPathTraversalRealWorld(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env and tools")
|
||||||
|
}
|
||||||
|
|
||||||
// This test ensures our protection works with actual file operations
|
// This test ensures our protection works with actual file operations
|
||||||
|
|
||||||
// Build cheat
|
// Build cheat
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("Failed to build: %v\n%s", err, output)
|
t.Fatalf("Failed to build: %v\n%s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build integration
|
//go:build integration
|
||||||
|
|
||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
@@ -17,12 +16,10 @@ import (
|
|||||||
|
|
||||||
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
||||||
func BenchmarkSearchCommand(b *testing.B) {
|
func BenchmarkSearchCommand(b *testing.B) {
|
||||||
|
root := repoRootBench(b)
|
||||||
|
|
||||||
// Build the cheat binary in .tmp (using absolute path)
|
// Build the cheat binary in .tmp (using absolute path)
|
||||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to get root dir: %v", err)
|
|
||||||
}
|
|
||||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
b.Fatalf("Failed to create temp dir: %v", err)
|
b.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -35,7 +32,7 @@ func BenchmarkSearchCommand(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||||
cmd.Dir = rootDir
|
cmd.Dir = root
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
@@ -108,23 +105,15 @@ cheatpaths:
|
|||||||
cmd := exec.Command(cheatBin, tc.args...)
|
cmd := exec.Command(cheatBin, tc.args...)
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
|
|
||||||
// Capture output to prevent spamming
|
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
elapsed := time.Since(start)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
|
b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report custom metric
|
|
||||||
b.ReportMetric(float64(elapsed.Nanoseconds())/1e6, "ms/op")
|
|
||||||
|
|
||||||
// Ensure we got some results
|
|
||||||
if stdout.Len() == 0 {
|
if stdout.Len() == 0 {
|
||||||
b.Fatal("No output from search")
|
b.Fatal("No output from search")
|
||||||
}
|
}
|
||||||
@@ -135,12 +124,10 @@ cheatpaths:
|
|||||||
|
|
||||||
// BenchmarkListCommand benchmarks the list command for comparison
|
// BenchmarkListCommand benchmarks the list command for comparison
|
||||||
func BenchmarkListCommand(b *testing.B) {
|
func BenchmarkListCommand(b *testing.B) {
|
||||||
|
root := repoRootBench(b)
|
||||||
|
|
||||||
// Build the cheat binary in .tmp (using absolute path)
|
// Build the cheat binary in .tmp (using absolute path)
|
||||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to get root dir: %v", err)
|
|
||||||
}
|
|
||||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
b.Fatalf("Failed to create temp dir: %v", err)
|
b.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -153,7 +140,7 @@ func BenchmarkListCommand(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||||
cmd.Dir = rootDir
|
cmd.Dir = root
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
3
vendor/dario.cat/mergo/.gitignore
vendored
3
vendor/dario.cat/mergo/.gitignore
vendored
@@ -13,6 +13,9 @@
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
# Golang/Intellij
|
||||||
|
.idea
|
||||||
|
|
||||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||||
.glide/
|
.glide/
|
||||||
|
|
||||||
|
|||||||
7
vendor/dario.cat/mergo/FUNDING.json
vendored
Normal file
7
vendor/dario.cat/mergo/FUNDING.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"drips": {
|
||||||
|
"ethereum": {
|
||||||
|
"ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
vendor/dario.cat/mergo/README.md
vendored
105
vendor/dario.cat/mergo/README.md
vendored
@@ -44,13 +44,21 @@ Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
|
Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild).
|
||||||
|
|
||||||
|
No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases.
|
||||||
|
|
||||||
### Important notes
|
### Important notes
|
||||||
|
|
||||||
#### 1.0.0
|
#### 1.0.0
|
||||||
|
|
||||||
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`.
|
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released.
|
||||||
|
|
||||||
|
If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
|
||||||
|
```
|
||||||
|
|
||||||
#### 0.3.9
|
#### 0.3.9
|
||||||
|
|
||||||
@@ -64,55 +72,23 @@ If you were using Mergo before April 6th, 2015, please check your project works
|
|||||||
|
|
||||||
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
|
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
|
||||||
|
|
||||||
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
|
||||||
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
|
||||||
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
|
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
|
||||||
|
|
||||||
### Mergo in the wild
|
### Mergo in the wild
|
||||||
|
|
||||||
- [moby/moby](https://github.com/moby/moby)
|
Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including:
|
||||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
|
|
||||||
- [vmware/dispatch](https://github.com/vmware/dispatch)
|
* [containerd/containerd](https://github.com/containerd/containerd)
|
||||||
- [Shopify/themekit](https://github.com/Shopify/themekit)
|
* [datadog/datadog-agent](https://github.com/datadog/datadog-agent)
|
||||||
- [imdario/zas](https://github.com/imdario/zas)
|
* [docker/cli/](https://github.com/docker/cli/)
|
||||||
- [matcornic/hermes](https://github.com/matcornic/hermes)
|
* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
|
||||||
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
|
* [go-micro/go-micro](https://github.com/go-micro/go-micro)
|
||||||
- [kataras/iris](https://github.com/kataras/iris)
|
* [grafana/loki](https://github.com/grafana/loki)
|
||||||
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
|
* [masterminds/sprig](github.com/Masterminds/sprig)
|
||||||
- [go-task/task](https://github.com/go-task/task)
|
* [moby/moby](https://github.com/moby/moby)
|
||||||
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
|
* [slackhq/nebula](https://github.com/slackhq/nebula)
|
||||||
- [ory/hydra](https://github.com/ory/hydra)
|
* [volcano-sh/volcano](https://github.com/volcano-sh/volcano)
|
||||||
- [sisatech/vcli](https://github.com/sisatech/vcli)
|
|
||||||
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
|
|
||||||
- [projectcalico/felix](https://github.com/projectcalico/felix)
|
|
||||||
- [resin-os/balena](https://github.com/resin-os/balena)
|
|
||||||
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
|
|
||||||
- [Telefonica/govice](https://github.com/Telefonica/govice)
|
|
||||||
- [supergiant/supergiant](supergiant/supergiant)
|
|
||||||
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
|
|
||||||
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
|
|
||||||
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
|
|
||||||
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
|
|
||||||
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
|
|
||||||
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
|
|
||||||
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
|
|
||||||
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
|
|
||||||
- [divshot/gitling](https://github.com/divshot/gitling)
|
|
||||||
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
|
|
||||||
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
|
|
||||||
- [elwinar/rambler](https://github.com/elwinar/rambler)
|
|
||||||
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
|
|
||||||
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
|
|
||||||
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
|
|
||||||
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
|
|
||||||
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
|
|
||||||
- [thoas/picfit](https://github.com/thoas/picfit)
|
|
||||||
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
|
|
||||||
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
|
|
||||||
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
|
|
||||||
- [containerssh/containerssh](https://github.com/containerssh/containerssh)
|
|
||||||
- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
|
|
||||||
- [tjpnz/structbot](https://github.com/tjpnz/structbot)
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -141,6 +117,39 @@ if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"dario.cat/mergo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Foo struct {
|
||||||
|
A *string
|
||||||
|
B int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
first := "first"
|
||||||
|
second := "second"
|
||||||
|
src := Foo{
|
||||||
|
A: &first,
|
||||||
|
B: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := Foo{
|
||||||
|
A: &second,
|
||||||
|
B: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
|
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -181,10 +190,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: if test are failing due missing package, please execute:
|
|
||||||
|
|
||||||
go get gopkg.in/yaml.v3
|
|
||||||
|
|
||||||
### Transformers
|
### Transformers
|
||||||
|
|
||||||
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
|
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
|
||||||
|
|||||||
4
vendor/dario.cat/mergo/SECURITY.md
vendored
4
vendor/dario.cat/mergo/SECURITY.md
vendored
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 0.3.x | :white_check_mark: |
|
| 1.x.x | :white_check_mark: |
|
||||||
| < 0.3 | :x: |
|
| < 1.0 | :x: |
|
||||||
|
|
||||||
## Security contact information
|
## Security contact information
|
||||||
|
|
||||||
|
|||||||
2
vendor/dario.cat/mergo/map.go
vendored
2
vendor/dario.cat/mergo/map.go
vendored
@@ -58,7 +58,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf
|
|||||||
}
|
}
|
||||||
fieldName := field.Name
|
fieldName := field.Name
|
||||||
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
fieldName = changeInitialCase(fieldName, unicode.ToLower)
|
||||||
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
|
if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue {
|
||||||
dstMap[fieldName] = src.Field(i).Interface()
|
dstMap[fieldName] = src.Field(i).Interface()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
vendor/dario.cat/mergo/merge.go
vendored
2
vendor/dario.cat/mergo/merge.go
vendored
@@ -269,7 +269,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
|
|||||||
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
|
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else if src.Elem().Kind() != reflect.Struct {
|
||||||
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
|
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
|
||||||
dst.Set(src)
|
dst.Set(src)
|
||||||
}
|
}
|
||||||
|
|||||||
10
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
10
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
@@ -1,7 +1,3 @@
|
|||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- pkg/etw/sample
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
# style
|
# style
|
||||||
@@ -20,9 +16,13 @@ linters:
|
|||||||
- gofmt # files are gofmt'ed
|
- gofmt # files are gofmt'ed
|
||||||
- gosec # security
|
- gosec # security
|
||||||
- nilerr # returns nil even with non-nil error
|
- nilerr # returns nil even with non-nil error
|
||||||
|
- thelper # test helpers without t.Helper()
|
||||||
- unparam # unused function params
|
- unparam # unused function params
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- pkg/etw/sample
|
||||||
|
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# err is very often shadowed in nested scopes
|
# err is very often shadowed in nested scopes
|
||||||
- linters:
|
- linters:
|
||||||
@@ -69,9 +69,7 @@ linters-settings:
|
|||||||
# struct order is often for Win32 compat
|
# struct order is often for Win32 compat
|
||||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||||
- fieldalignment
|
- fieldalignment
|
||||||
check-shadowing: true
|
|
||||||
nolintlint:
|
nolintlint:
|
||||||
allow-leading-space: false
|
|
||||||
require-explanation: true
|
require-explanation: true
|
||||||
require-specific: true
|
require-specific: true
|
||||||
revive:
|
revive:
|
||||||
|
|||||||
33
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
33
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
@@ -10,14 +10,14 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio/internal/fs"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
//sys backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||||
//sys backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BackupData = uint32(iota + 1)
|
BackupData = uint32(iota + 1)
|
||||||
@@ -104,7 +104,7 @@ func (r *BackupStreamReader) Next() (*BackupHeader, error) {
|
|||||||
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
hdr.Name = syscall.UTF16ToString(name)
|
hdr.Name = windows.UTF16ToString(name)
|
||||||
}
|
}
|
||||||
if wsi.StreamID == BackupSparseBlock {
|
if wsi.StreamID == BackupSparseBlock {
|
||||||
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
||||||
@@ -205,7 +205,7 @@ func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
|
|||||||
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
||||||
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||||
var bytesRead uint32
|
var bytesRead uint32
|
||||||
err := backupRead(syscall.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ func (r *BackupFileReader) Read(b []byte) (int, error) {
|
|||||||
// the underlying file.
|
// the underlying file.
|
||||||
func (r *BackupFileReader) Close() error {
|
func (r *BackupFileReader) Close() error {
|
||||||
if r.ctx != 0 {
|
if r.ctx != 0 {
|
||||||
_ = backupRead(syscall.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||||
runtime.KeepAlive(r.f)
|
runtime.KeepAlive(r.f)
|
||||||
r.ctx = 0
|
r.ctx = 0
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
|
|||||||
// Write restores a portion of the file using the provided backup stream.
|
// Write restores a portion of the file using the provided backup stream.
|
||||||
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||||
var bytesWritten uint32
|
var bytesWritten uint32
|
||||||
err := backupWrite(syscall.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
|||||||
// close the underlying file.
|
// close the underlying file.
|
||||||
func (w *BackupFileWriter) Close() error {
|
func (w *BackupFileWriter) Close() error {
|
||||||
if w.ctx != 0 {
|
if w.ctx != 0 {
|
||||||
_ = backupWrite(syscall.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||||
runtime.KeepAlive(w.f)
|
runtime.KeepAlive(w.f)
|
||||||
w.ctx = 0
|
w.ctx = 0
|
||||||
}
|
}
|
||||||
@@ -271,17 +271,14 @@ func (w *BackupFileWriter) Close() error {
|
|||||||
//
|
//
|
||||||
// If the file opened was a directory, it cannot be used with Readdir().
|
// If the file opened was a directory, it cannot be used with Readdir().
|
||||||
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
||||||
winPath, err := syscall.UTF16FromString(path)
|
h, err := fs.CreateFile(path,
|
||||||
if err != nil {
|
fs.AccessMask(access),
|
||||||
return nil, err
|
fs.FileShareMode(share),
|
||||||
}
|
|
||||||
h, err := syscall.CreateFile(&winPath[0],
|
|
||||||
access,
|
|
||||||
share,
|
|
||||||
nil,
|
nil,
|
||||||
createmode,
|
fs.FileCreationDisposition(createmode),
|
||||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT,
|
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||||
0)
|
0,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = &os.PathError{Op: "open", Path: path, Err: err}
|
err = &os.PathError{Op: "open", Path: path, Err: err}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
85
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
85
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
@@ -15,26 +15,11 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
//sys cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) = CancelIoEx
|
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
|
||||||
//sys createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) = CreateIoCompletionPort
|
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
|
||||||
//sys getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||||
//sys setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||||
//sys wsaGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||||
|
|
||||||
type atomicBool int32
|
|
||||||
|
|
||||||
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
|
|
||||||
func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }
|
|
||||||
func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) }
|
|
||||||
|
|
||||||
//revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg
|
|
||||||
func (b *atomicBool) swap(new bool) bool {
|
|
||||||
var newInt int32
|
|
||||||
if new {
|
|
||||||
newInt = 1
|
|
||||||
}
|
|
||||||
return atomic.SwapInt32((*int32)(b), newInt) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFileClosed = errors.New("file has already been closed")
|
ErrFileClosed = errors.New("file has already been closed")
|
||||||
@@ -50,7 +35,7 @@ func (*timeoutError) Temporary() bool { return true }
|
|||||||
type timeoutChan chan struct{}
|
type timeoutChan chan struct{}
|
||||||
|
|
||||||
var ioInitOnce sync.Once
|
var ioInitOnce sync.Once
|
||||||
var ioCompletionPort syscall.Handle
|
var ioCompletionPort windows.Handle
|
||||||
|
|
||||||
// ioResult contains the result of an asynchronous IO operation.
|
// ioResult contains the result of an asynchronous IO operation.
|
||||||
type ioResult struct {
|
type ioResult struct {
|
||||||
@@ -60,12 +45,12 @@ type ioResult struct {
|
|||||||
|
|
||||||
// ioOperation represents an outstanding asynchronous Win32 IO.
|
// ioOperation represents an outstanding asynchronous Win32 IO.
|
||||||
type ioOperation struct {
|
type ioOperation struct {
|
||||||
o syscall.Overlapped
|
o windows.Overlapped
|
||||||
ch chan ioResult
|
ch chan ioResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func initIO() {
|
func initIO() {
|
||||||
h, err := createIoCompletionPort(syscall.InvalidHandle, 0, 0, 0xffffffff)
|
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -76,10 +61,10 @@ func initIO() {
|
|||||||
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
||||||
// It takes ownership of this handle and will close it if it is garbage collected.
|
// It takes ownership of this handle and will close it if it is garbage collected.
|
||||||
type win32File struct {
|
type win32File struct {
|
||||||
handle syscall.Handle
|
handle windows.Handle
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
wgLock sync.RWMutex
|
wgLock sync.RWMutex
|
||||||
closing atomicBool
|
closing atomic.Bool
|
||||||
socket bool
|
socket bool
|
||||||
readDeadline deadlineHandler
|
readDeadline deadlineHandler
|
||||||
writeDeadline deadlineHandler
|
writeDeadline deadlineHandler
|
||||||
@@ -90,11 +75,11 @@ type deadlineHandler struct {
|
|||||||
channel timeoutChan
|
channel timeoutChan
|
||||||
channelLock sync.RWMutex
|
channelLock sync.RWMutex
|
||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
timedout atomicBool
|
timedout atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeWin32File makes a new win32File from an existing file handle.
|
// makeWin32File makes a new win32File from an existing file handle.
|
||||||
func makeWin32File(h syscall.Handle) (*win32File, error) {
|
func makeWin32File(h windows.Handle) (*win32File, error) {
|
||||||
f := &win32File{handle: h}
|
f := &win32File{handle: h}
|
||||||
ioInitOnce.Do(initIO)
|
ioInitOnce.Do(initIO)
|
||||||
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
||||||
@@ -110,7 +95,12 @@ func makeWin32File(h syscall.Handle) (*win32File, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use NewOpenFile instead.
|
||||||
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||||
|
return NewOpenFile(windows.Handle(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
|
||||||
// If we return the result of makeWin32File directly, it can result in an
|
// If we return the result of makeWin32File directly, it can result in an
|
||||||
// interface-wrapped nil, rather than a nil interface value.
|
// interface-wrapped nil, rather than a nil interface value.
|
||||||
f, err := makeWin32File(h)
|
f, err := makeWin32File(h)
|
||||||
@@ -124,13 +114,13 @@ func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
|||||||
func (f *win32File) closeHandle() {
|
func (f *win32File) closeHandle() {
|
||||||
f.wgLock.Lock()
|
f.wgLock.Lock()
|
||||||
// Atomically set that we are closing, releasing the resources only once.
|
// Atomically set that we are closing, releasing the resources only once.
|
||||||
if !f.closing.swap(true) {
|
if !f.closing.Swap(true) {
|
||||||
f.wgLock.Unlock()
|
f.wgLock.Unlock()
|
||||||
// cancel all IO and wait for it to complete
|
// cancel all IO and wait for it to complete
|
||||||
_ = cancelIoEx(f.handle, nil)
|
_ = cancelIoEx(f.handle, nil)
|
||||||
f.wg.Wait()
|
f.wg.Wait()
|
||||||
// at this point, no new IO can start
|
// at this point, no new IO can start
|
||||||
syscall.Close(f.handle)
|
windows.Close(f.handle)
|
||||||
f.handle = 0
|
f.handle = 0
|
||||||
} else {
|
} else {
|
||||||
f.wgLock.Unlock()
|
f.wgLock.Unlock()
|
||||||
@@ -145,14 +135,14 @@ func (f *win32File) Close() error {
|
|||||||
|
|
||||||
// IsClosed checks if the file has been closed.
|
// IsClosed checks if the file has been closed.
|
||||||
func (f *win32File) IsClosed() bool {
|
func (f *win32File) IsClosed() bool {
|
||||||
return f.closing.isSet()
|
return f.closing.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareIO prepares for a new IO operation.
|
// prepareIO prepares for a new IO operation.
|
||||||
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
||||||
func (f *win32File) prepareIO() (*ioOperation, error) {
|
func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||||
f.wgLock.RLock()
|
f.wgLock.RLock()
|
||||||
if f.closing.isSet() {
|
if f.closing.Load() {
|
||||||
f.wgLock.RUnlock()
|
f.wgLock.RUnlock()
|
||||||
return nil, ErrFileClosed
|
return nil, ErrFileClosed
|
||||||
}
|
}
|
||||||
@@ -164,12 +154,12 @@ func (f *win32File) prepareIO() (*ioOperation, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ioCompletionProcessor processes completed async IOs forever.
|
// ioCompletionProcessor processes completed async IOs forever.
|
||||||
func ioCompletionProcessor(h syscall.Handle) {
|
func ioCompletionProcessor(h windows.Handle) {
|
||||||
for {
|
for {
|
||||||
var bytes uint32
|
var bytes uint32
|
||||||
var key uintptr
|
var key uintptr
|
||||||
var op *ioOperation
|
var op *ioOperation
|
||||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, syscall.INFINITE)
|
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
|
||||||
if op == nil {
|
if op == nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -182,11 +172,11 @@ func ioCompletionProcessor(h syscall.Handle) {
|
|||||||
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
||||||
// the operation has actually completed.
|
// the operation has actually completed.
|
||||||
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
||||||
if err != syscall.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||||
return int(bytes), err
|
return int(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.closing.isSet() {
|
if f.closing.Load() {
|
||||||
_ = cancelIoEx(f.handle, &c.o)
|
_ = cancelIoEx(f.handle, &c.o)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +191,8 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
|||||||
select {
|
select {
|
||||||
case r = <-c.ch:
|
case r = <-c.ch:
|
||||||
err = r.err
|
err = r.err
|
||||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||||
if f.closing.isSet() {
|
if f.closing.Load() {
|
||||||
err = ErrFileClosed
|
err = ErrFileClosed
|
||||||
}
|
}
|
||||||
} else if err != nil && f.socket {
|
} else if err != nil && f.socket {
|
||||||
@@ -214,7 +204,7 @@ func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, er
|
|||||||
_ = cancelIoEx(f.handle, &c.o)
|
_ = cancelIoEx(f.handle, &c.o)
|
||||||
r = <-c.ch
|
r = <-c.ch
|
||||||
err = r.err
|
err = r.err
|
||||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||||
err = ErrTimeout
|
err = ErrTimeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,23 +225,22 @@ func (f *win32File) Read(b []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
defer f.wg.Done()
|
defer f.wg.Done()
|
||||||
|
|
||||||
if f.readDeadline.timedout.isSet() {
|
if f.readDeadline.timedout.Load() {
|
||||||
return 0, ErrTimeout
|
return 0, ErrTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes uint32
|
var bytes uint32
|
||||||
err = syscall.ReadFile(f.handle, b, &bytes, &c.o)
|
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
|
||||||
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
||||||
runtime.KeepAlive(b)
|
runtime.KeepAlive(b)
|
||||||
|
|
||||||
// Handle EOF conditions.
|
// Handle EOF conditions.
|
||||||
if err == nil && n == 0 && len(b) != 0 {
|
if err == nil && n == 0 && len(b) != 0 {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
} else if err == syscall.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
} else {
|
|
||||||
return n, err
|
|
||||||
}
|
}
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes to a file handle.
|
// Write writes to a file handle.
|
||||||
@@ -262,12 +251,12 @@ func (f *win32File) Write(b []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
defer f.wg.Done()
|
defer f.wg.Done()
|
||||||
|
|
||||||
if f.writeDeadline.timedout.isSet() {
|
if f.writeDeadline.timedout.Load() {
|
||||||
return 0, ErrTimeout
|
return 0, ErrTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes uint32
|
var bytes uint32
|
||||||
err = syscall.WriteFile(f.handle, b, &bytes, &c.o)
|
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
|
||||||
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
||||||
runtime.KeepAlive(b)
|
runtime.KeepAlive(b)
|
||||||
return n, err
|
return n, err
|
||||||
@@ -282,7 +271,7 @@ func (f *win32File) SetWriteDeadline(deadline time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *win32File) Flush() error {
|
func (f *win32File) Flush() error {
|
||||||
return syscall.FlushFileBuffers(f.handle)
|
return windows.FlushFileBuffers(f.handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *win32File) Fd() uintptr {
|
func (f *win32File) Fd() uintptr {
|
||||||
@@ -299,7 +288,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
|
|||||||
}
|
}
|
||||||
d.timer = nil
|
d.timer = nil
|
||||||
}
|
}
|
||||||
d.timedout.setFalse()
|
d.timedout.Store(false)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-d.channel:
|
case <-d.channel:
|
||||||
@@ -314,7 +303,7 @@ func (d *deadlineHandler) set(deadline time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeoutIO := func() {
|
timeoutIO := func() {
|
||||||
d.timedout.setTrue()
|
d.timedout.Store(true)
|
||||||
close(d.channel)
|
close(d.channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
22
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
@@ -18,9 +18,18 @@ type FileBasicInfo struct {
|
|||||||
_ uint32 // padding
|
_ uint32 // padding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
|
||||||
|
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
|
||||||
|
// alignment is necessary to pass this as FILE_BASIC_INFO.
|
||||||
|
type alignedFileBasicInfo struct {
|
||||||
|
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
|
||||||
|
FileAttributes uint32
|
||||||
|
_ uint32 // padding
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileBasicInfo retrieves times and attributes for a file.
|
// GetFileBasicInfo retrieves times and attributes for a file.
|
||||||
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||||
bi := &FileBasicInfo{}
|
bi := &alignedFileBasicInfo{}
|
||||||
if err := windows.GetFileInformationByHandleEx(
|
if err := windows.GetFileInformationByHandleEx(
|
||||||
windows.Handle(f.Fd()),
|
windows.Handle(f.Fd()),
|
||||||
windows.FileBasicInfo,
|
windows.FileBasicInfo,
|
||||||
@@ -30,16 +39,21 @@ func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
|||||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||||
}
|
}
|
||||||
runtime.KeepAlive(f)
|
runtime.KeepAlive(f)
|
||||||
return bi, nil
|
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
|
||||||
|
// public API of this module. The data may be unnecessarily aligned.
|
||||||
|
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFileBasicInfo sets times and attributes for a file.
|
// SetFileBasicInfo sets times and attributes for a file.
|
||||||
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
||||||
|
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
|
||||||
|
// suitable to pass to GetFileInformationByHandleEx.
|
||||||
|
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
|
||||||
if err := windows.SetFileInformationByHandle(
|
if err := windows.SetFileInformationByHandle(
|
||||||
windows.Handle(f.Fd()),
|
windows.Handle(f.Fd()),
|
||||||
windows.FileBasicInfo,
|
windows.FileBasicInfo,
|
||||||
(*byte)(unsafe.Pointer(bi)),
|
(*byte)(unsafe.Pointer(&biAligned)),
|
||||||
uint32(unsafe.Sizeof(*bi)),
|
uint32(unsafe.Sizeof(biAligned)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
||||||
}
|
}
|
||||||
|
|||||||
47
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
47
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
@@ -10,7 +10,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -181,13 +180,13 @@ type HvsockConn struct {
|
|||||||
var _ net.Conn = &HvsockConn{}
|
var _ net.Conn = &HvsockConn{}
|
||||||
|
|
||||||
func newHVSocket() (*win32File, error) {
|
func newHVSocket() (*win32File, error) {
|
||||||
fd, err := syscall.Socket(afHVSock, syscall.SOCK_STREAM, 1)
|
fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, os.NewSyscallError("socket", err)
|
return nil, os.NewSyscallError("socket", err)
|
||||||
}
|
}
|
||||||
f, err := makeWin32File(fd)
|
f, err := makeWin32File(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
syscall.Close(fd)
|
windows.Close(fd)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.socket = true
|
f.socket = true
|
||||||
@@ -197,16 +196,24 @@ func newHVSocket() (*win32File, error) {
|
|||||||
// ListenHvsock listens for connections on the specified hvsock address.
|
// ListenHvsock listens for connections on the specified hvsock address.
|
||||||
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
|
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
|
||||||
l := &HvsockListener{addr: *addr}
|
l := &HvsockListener{addr: *addr}
|
||||||
sock, err := newHVSocket()
|
|
||||||
|
var sock *win32File
|
||||||
|
sock, err = newHVSocket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, l.opErr("listen", err)
|
return nil, l.opErr("listen", err)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
_ = sock.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
sa := addr.raw()
|
sa := addr.raw()
|
||||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
err = socket.Bind(sock.handle, &sa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
|
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
|
||||||
}
|
}
|
||||||
err = syscall.Listen(sock.handle, 16)
|
err = windows.Listen(sock.handle, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
|
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
|
||||||
}
|
}
|
||||||
@@ -246,7 +253,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
|||||||
var addrbuf [addrlen * 2]byte
|
var addrbuf [addrlen * 2]byte
|
||||||
|
|
||||||
var bytes uint32
|
var bytes uint32
|
||||||
err = syscall.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
|
err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o)
|
||||||
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
|
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
|
||||||
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
|
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
|
||||||
}
|
}
|
||||||
@@ -263,7 +270,7 @@ func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
|||||||
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
|
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
|
||||||
|
|
||||||
// initialize the accepted socket and update its properties with those of the listening socket
|
// initialize the accepted socket and update its properties with those of the listening socket
|
||||||
if err = windows.Setsockopt(windows.Handle(sock.handle),
|
if err = windows.Setsockopt(sock.handle,
|
||||||
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
|
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
|
||||||
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
|
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
|
||||||
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
|
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
|
||||||
@@ -334,7 +341,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
sa := addr.raw()
|
sa := addr.raw()
|
||||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
err = socket.Bind(sock.handle, &sa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
|
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
|
||||||
}
|
}
|
||||||
@@ -347,7 +354,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
|||||||
var bytes uint32
|
var bytes uint32
|
||||||
for i := uint(0); i <= d.Retries; i++ {
|
for i := uint(0); i <= d.Retries; i++ {
|
||||||
err = socket.ConnectEx(
|
err = socket.ConnectEx(
|
||||||
windows.Handle(sock.handle),
|
sock.handle,
|
||||||
&sa,
|
&sa,
|
||||||
nil, // sendBuf
|
nil, // sendBuf
|
||||||
0, // sendDataLen
|
0, // sendDataLen
|
||||||
@@ -367,7 +374,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
|||||||
|
|
||||||
// update the connection properties, so shutdown can be used
|
// update the connection properties, so shutdown can be used
|
||||||
if err = windows.Setsockopt(
|
if err = windows.Setsockopt(
|
||||||
windows.Handle(sock.handle),
|
sock.handle,
|
||||||
windows.SOL_SOCKET,
|
windows.SOL_SOCKET,
|
||||||
windows.SO_UPDATE_CONNECT_CONTEXT,
|
windows.SO_UPDATE_CONNECT_CONTEXT,
|
||||||
nil, // optvalue
|
nil, // optvalue
|
||||||
@@ -378,7 +385,7 @@ func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *Hvsock
|
|||||||
|
|
||||||
// get the local name
|
// get the local name
|
||||||
var sal rawHvsockAddr
|
var sal rawHvsockAddr
|
||||||
err = socket.GetSockName(windows.Handle(sock.handle), &sal)
|
err = socket.GetSockName(sock.handle, &sal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
|
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
|
||||||
}
|
}
|
||||||
@@ -421,7 +428,7 @@ func (d *HvsockDialer) redialWait(ctx context.Context) (err error) {
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// assumes error is a plain, unwrapped syscall.Errno provided by direct syscall.
|
// assumes error is a plain, unwrapped windows.Errno provided by direct syscall.
|
||||||
func canRedial(err error) bool {
|
func canRedial(err error) bool {
|
||||||
//nolint:errorlint // guaranteed to be an Errno
|
//nolint:errorlint // guaranteed to be an Errno
|
||||||
switch err {
|
switch err {
|
||||||
@@ -447,9 +454,9 @@ func (conn *HvsockConn) Read(b []byte) (int, error) {
|
|||||||
return 0, conn.opErr("read", err)
|
return 0, conn.opErr("read", err)
|
||||||
}
|
}
|
||||||
defer conn.sock.wg.Done()
|
defer conn.sock.wg.Done()
|
||||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||||
var flags, bytes uint32
|
var flags, bytes uint32
|
||||||
err = syscall.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
||||||
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
|
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var eno windows.Errno
|
var eno windows.Errno
|
||||||
@@ -482,9 +489,9 @@ func (conn *HvsockConn) write(b []byte) (int, error) {
|
|||||||
return 0, conn.opErr("write", err)
|
return 0, conn.opErr("write", err)
|
||||||
}
|
}
|
||||||
defer conn.sock.wg.Done()
|
defer conn.sock.wg.Done()
|
||||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||||
var bytes uint32
|
var bytes uint32
|
||||||
err = syscall.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
||||||
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
|
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var eno windows.Errno
|
var eno windows.Errno
|
||||||
@@ -511,7 +518,7 @@ func (conn *HvsockConn) shutdown(how int) error {
|
|||||||
return socket.ErrSocketClosed
|
return socket.ErrSocketClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
err := syscall.Shutdown(conn.sock.handle, how)
|
err := windows.Shutdown(conn.sock.handle, how)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the connection was closed, shutdowns fail with "not connected"
|
// If the connection was closed, shutdowns fail with "not connected"
|
||||||
if errors.Is(err, windows.WSAENOTCONN) ||
|
if errors.Is(err, windows.WSAENOTCONN) ||
|
||||||
@@ -525,7 +532,7 @@ func (conn *HvsockConn) shutdown(how int) error {
|
|||||||
|
|
||||||
// CloseRead shuts down the read end of the socket, preventing future read operations.
|
// CloseRead shuts down the read end of the socket, preventing future read operations.
|
||||||
func (conn *HvsockConn) CloseRead() error {
|
func (conn *HvsockConn) CloseRead() error {
|
||||||
err := conn.shutdown(syscall.SHUT_RD)
|
err := conn.shutdown(windows.SHUT_RD)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conn.opErr("closeread", err)
|
return conn.opErr("closeread", err)
|
||||||
}
|
}
|
||||||
@@ -535,7 +542,7 @@ func (conn *HvsockConn) CloseRead() error {
|
|||||||
// CloseWrite shuts down the write end of the socket, preventing future write operations and
|
// CloseWrite shuts down the write end of the socket, preventing future write operations and
|
||||||
// notifying the other endpoint that no more data will be written.
|
// notifying the other endpoint that no more data will be written.
|
||||||
func (conn *HvsockConn) CloseWrite() error {
|
func (conn *HvsockConn) CloseWrite() error {
|
||||||
err := conn.shutdown(syscall.SHUT_WR)
|
err := conn.shutdown(windows.SHUT_WR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conn.opErr("closewrite", err)
|
return conn.opErr("closewrite", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user