mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
1 Commits
4.5.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
710eb46173 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,2 @@
|
||||
dist
|
||||
tags
|
||||
.tmp
|
||||
*.test
|
||||
.claude
|
||||
|
||||
117
CLAUDE.md
117
CLAUDE.md
@@ -1,117 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build for your architecture
|
||||
make build
|
||||
|
||||
# Build release binaries for all platforms
|
||||
make build-release
|
||||
|
||||
# Install cheat to your PATH
|
||||
make install
|
||||
```
|
||||
|
||||
### Testing and Quality Checks
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
go test ./...
|
||||
|
||||
# Run a single test
|
||||
go test -run TestFunctionName ./internal/package_name
|
||||
|
||||
# Generate test coverage report
|
||||
make coverage
|
||||
|
||||
# Run linter (revive)
|
||||
make lint
|
||||
|
||||
# Run go vet
|
||||
make vet
|
||||
|
||||
# Format code
|
||||
make fmt
|
||||
|
||||
# Run all checks (vendor, fmt, lint, vet, test)
|
||||
make check
|
||||
```
|
||||
|
||||
### Development Setup
|
||||
```bash
|
||||
# Install development dependencies (revive linter, scc)
|
||||
make setup
|
||||
|
||||
# Update and verify vendored dependencies
|
||||
make vendor-update
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The `cheat` command-line tool is organized into several key packages:
|
||||
|
||||
### Command Layer (`cmd/cheat/`)
|
||||
- `main.go`: Entry point, argument parsing, command routing
|
||||
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
||||
- Commands are selected based on docopt parsed arguments
|
||||
|
||||
### Core Internal Packages
|
||||
|
||||
1. **`internal/config`**: Configuration management
|
||||
- Loads YAML config from platform-specific paths
|
||||
- Manages editor, pager, colorization settings
|
||||
- Validates and expands cheatpath configurations
|
||||
|
||||
2. **`internal/cheatpath`**: Cheatsheet path management
|
||||
- Represents collections of cheatsheets on filesystem
|
||||
- Handles read-only vs writable paths
|
||||
- Supports filtering and validation
|
||||
|
||||
3. **`internal/sheet`**: Individual cheatsheet handling
|
||||
- Parses YAML frontmatter for tags and syntax
|
||||
- Implements syntax highlighting via Chroma
|
||||
- Provides search functionality within sheets
|
||||
|
||||
4. **`internal/sheets`**: Collection operations
|
||||
- Loads sheets from multiple cheatpaths
|
||||
- Consolidates duplicates (local overrides global)
|
||||
- Filters by tags and sorts results
|
||||
|
||||
5. **`internal/display`**: Output formatting
|
||||
- Writes to stdout or pager
|
||||
- Handles text formatting and indentation
|
||||
|
||||
6. **`internal/repo`**: Git repository management
|
||||
- Clones community cheatsheet repositories
|
||||
- Updates existing repositories
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Filesystem-based storage**: Cheatsheets are plain text files
|
||||
- **Override mechanism**: Local sheets override community sheets with same name
|
||||
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
||||
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
||||
|
||||
### Sheet Format
|
||||
|
||||
Cheatsheets are plain text files optionally prefixed with YAML frontmatter:
|
||||
```
|
||||
---
|
||||
syntax: bash
|
||||
tags: [ networking, ssh ]
|
||||
---
|
||||
# SSH tunneling example
|
||||
ssh -L 8080:localhost:80 user@remote
|
||||
```
|
||||
|
||||
### Working with the Codebase
|
||||
|
||||
- Always check for `.git` directories and skip them during filesystem walks
|
||||
- Use `go-git` for repository operations, not exec'ing git commands
|
||||
- Platform-specific paths are handled in `internal/config/paths.go`
|
||||
- Color output uses ANSI codes via the Chroma library
|
||||
- Test files use the `internal/mock` package for test data
|
||||
@@ -1,14 +1,48 @@
|
||||
Contributing
|
||||
CONTRIBUTING
|
||||
============
|
||||
Do you want to contribute to `cheat`? There are a few ways to help:
|
||||
|
||||
Thank you for your interest in `cheat`.
|
||||
#### Submit a cheatsheet ####
|
||||
Do you have a witty bash one-liner to share? [Open a pull-request][pr] against
|
||||
the [cheatsheets][] repository. (The `cheat` executable source code lives in
|
||||
[cheat/cheat][cheat]. Cheatsheet content lives in
|
||||
[cheat/cheatsheets][cheatsheets].)
|
||||
|
||||
Pull requests are no longer being accepted, and have been disabled on this
|
||||
repository. The maintainer is not currently reviewing or merging external code
|
||||
contributions.
|
||||
#### Report a bug ####
|
||||
Did you find a bug? Report it in the [issue tracker][issues]. (But before you
|
||||
do, please look through the open issues to make sure that it hasn't already
|
||||
been reported.)
|
||||
|
||||
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
|
||||
existing open issues to make sure it hasn't already been reported.
|
||||
#### Add a feature ####
|
||||
Do you have a feature that you'd like to contribute? Propose it in the [issue
|
||||
tracker][issues] to discuss with the maintainer whether it would be considered
|
||||
for merging.
|
||||
|
||||
[issues]: https://github.com/cheat/cheat/issues
|
||||
`cheat` is mostly mature and feature-complete, but may still have some room for
|
||||
new features. See [HACKING.md][hacking] for a quick-start guide to `cheat`
|
||||
development.
|
||||
|
||||
#### Add documentation ####
|
||||
Did you encounter features, bugs, edge-cases, use-cases, or environment
|
||||
considerations that were undocumented or under-documented? Add them to the
|
||||
[wiki][]. (You may also open a pull-request against the `README`, if
|
||||
appropriate.)
|
||||
|
||||
Do you enjoy technical writing or proofreading? Help keep the documentation
|
||||
error-free and well-organized.
|
||||
|
||||
#### Spread the word ####
|
||||
Are you unable to do the above, but still want to contribute? You can help
|
||||
`cheat` simply by telling others about it. Share it with friends and coworkers
|
||||
that might benefit from using it.
|
||||
|
||||
#### Pull Requests ####
|
||||
Please open all pull-requests against the `develop` branch.
|
||||
|
||||
|
||||
[cheat]: https://github.com/cheat/cheat
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[hacking]: HACKING.md
|
||||
[issues]: https://github.com/cheat/cheat/issues
|
||||
[pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork
|
||||
[wiki]: https://github.com/cheat/cheat/wiki
|
||||
|
||||
250
HACKING.md
250
HACKING.md
@@ -1,241 +1,57 @@
|
||||
# Hacking Guide
|
||||
Hacking
|
||||
=======
|
||||
The following is a quickstart guide for developing `cheat`.
|
||||
|
||||
This document provides a comprehensive guide for developing `cheat`, including setup, architecture overview, and code patterns.
|
||||
## 1. Install system dependencies
|
||||
Before you begin, you must install a handful of system dependencies. The
|
||||
following are required, and must be available on your `PATH`:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install system dependencies
|
||||
|
||||
The following are required and must be available on your `PATH`:
|
||||
- `git`
|
||||
- `go` (>= 1.19 is recommended)
|
||||
- `go` (>= 1.17 is recommended)
|
||||
- `make`
|
||||
|
||||
Optional dependencies:
|
||||
The following dependencies are optional:
|
||||
- `docker`
|
||||
- `pandoc` (necessary to generate a `man` page)
|
||||
|
||||
### 2. Install utility applications
|
||||
Run `make setup` to install `scc` and `revive`, which are used by various `make` targets.
|
||||
## 2. Install utility applications
|
||||
Run `make setup` to install `scc` and `revive`, which are used by various
|
||||
`make` targets.
|
||||
|
||||
### 3. Development workflow
|
||||
## 3. Development workflow
|
||||
After your environment has been configured, your development workflow will
|
||||
resemble the following:
|
||||
|
||||
1. Make changes to the `cheat` source code
|
||||
2. Run `make test` to run unit-tests
|
||||
3. Fix compiler errors and failing tests as necessary
|
||||
4. Run `make build`. A `cheat` executable will be written to the `dist` directory
|
||||
5. Use the new executable by running `dist/cheat <command>`
|
||||
6. Run `make install` to install `cheat` to your `PATH`
|
||||
7. Run `make build-release` to build cross-platform binaries in `dist`
|
||||
8. Run `make clean` to clean the `dist` directory when desired
|
||||
1. Make changes to the `cheat` source code.
|
||||
2. Run `make test` to run unit-tests.
|
||||
3. Fix compiler errors and failing tests as necessary.
|
||||
4. Run `make`. A `cheat` executable will be written to the `dist` directory.
|
||||
5. Use the new executable by running `dist/cheat <command>`.
|
||||
6. Run `make install` to install `cheat` to your `PATH`.
|
||||
7. Run `make build-release` to build cross-platform binaries in `dist`.
|
||||
8. Run `make clean` to clean the `dist` directory when desired.
|
||||
|
||||
You may run `make help` to see a list of available `make` commands.
|
||||
|
||||
### 4. Testing
|
||||
### Developing with docker
|
||||
It may be useful to test your changes within a pristine environment. An
|
||||
Alpine-based docker container has been provided for that purpose.
|
||||
|
||||
#### Unit Tests
|
||||
Run unit tests with:
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
Integration tests that require network access are separated using build tags. Run them with:
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
To run all tests (unit and integration):
|
||||
```bash
|
||||
make test-all
|
||||
```
|
||||
|
||||
#### Test Coverage
|
||||
Generate a coverage report with:
|
||||
```bash
|
||||
make coverage # HTML report
|
||||
make coverage-text # Terminal output
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Package Structure
|
||||
|
||||
The `cheat` application follows a clean architecture with well-separated concerns:
|
||||
|
||||
- **`cmd/cheat/`**: Command layer with argument parsing and command routing
|
||||
- **`internal/config`**: Configuration management (YAML loading, validation, paths)
|
||||
- **`internal/cheatpath`**: Cheatsheet path management (collections, filtering)
|
||||
- **`internal/sheet`**: Individual cheatsheet handling (parsing, search, highlighting)
|
||||
- **`internal/sheets`**: Collection operations (loading, consolidation, filtering)
|
||||
- **`internal/display`**: Output formatting (pager integration, colorization)
|
||||
- **`internal/repo`**: Git repository management for community sheets
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Filesystem-based storage**: Cheatsheets are plain text files
|
||||
- **Override mechanism**: Local sheets override community sheets with same name
|
||||
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
||||
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
||||
|
||||
## Core Types and Functions
|
||||
|
||||
### Config (`internal/config`)
|
||||
|
||||
The main configuration structure:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Colorize bool `yaml:"colorize"`
|
||||
Editor string `yaml:"editor"`
|
||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
||||
Style string `yaml:"style"`
|
||||
Formatter string `yaml:"formatter"`
|
||||
Pager string `yaml:"pager"`
|
||||
Path string
|
||||
}
|
||||
```
|
||||
|
||||
Key functions:
|
||||
- `New(opts, confPath, resolve)` - Load config from file
|
||||
- `Validate()` - Validate configuration values
|
||||
- `Editor()` - Get editor from environment or defaults (package-level function)
|
||||
- `Pager()` - Get pager from environment or defaults (package-level function)
|
||||
|
||||
### Cheatpath (`internal/cheatpath`)
|
||||
|
||||
Represents a directory containing cheatsheets:
|
||||
|
||||
```go
|
||||
type Cheatpath struct {
|
||||
Name string // Friendly name (e.g., "personal")
|
||||
Path string // Filesystem path
|
||||
Tags []string // Tags applied to all sheets in this path
|
||||
ReadOnly bool // Whether sheets can be modified
|
||||
}
|
||||
```
|
||||
|
||||
### Sheet (`internal/sheet`)
|
||||
|
||||
Represents an individual cheatsheet:
|
||||
|
||||
```go
|
||||
type Sheet struct {
|
||||
Title string // Sheet name (from filename)
|
||||
CheatPath string // Name of the cheatpath this sheet belongs to
|
||||
Path string // Full filesystem path
|
||||
Text string // Content (without frontmatter)
|
||||
Tags []string // Combined tags (from frontmatter + cheatpath)
|
||||
Syntax string // Syntax for highlighting
|
||||
ReadOnly bool // Whether sheet can be edited
|
||||
}
|
||||
```
|
||||
|
||||
Key methods:
|
||||
- `New(title, cheatpath, path, tags, readOnly)` - Load from file
|
||||
- `Search(reg)` - Search content with a compiled regexp
|
||||
- `Colorize(conf)` - Apply syntax highlighting (modifies sheet in place)
|
||||
- `Tagged(needle)` - Check if sheet has the given tag
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Loading and Displaying a Sheet
|
||||
|
||||
```go
|
||||
// Load sheet
|
||||
s, err := sheet.New("tar", "personal", "/path/to/tar", []string{"personal"}, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Apply syntax highlighting (modifies sheet in place)
|
||||
s.Colorize(conf)
|
||||
|
||||
// Display with pager
|
||||
display.Write(s.Text, conf)
|
||||
```
|
||||
|
||||
### Working with Sheet Collections
|
||||
|
||||
```go
|
||||
// Load all sheets from cheatpaths (returns a slice of maps, one per cheatpath)
|
||||
allSheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Consolidate to handle duplicates (later cheatpaths take precedence)
|
||||
consolidated := sheets.Consolidate(allSheets)
|
||||
|
||||
// Filter by tag (operates on the slice of maps)
|
||||
filtered := sheets.Filter(allSheets, []string{"networking"})
|
||||
|
||||
// Sort alphabetically (returns a sorted slice)
|
||||
sorted := sheets.Sort(consolidated)
|
||||
```
|
||||
|
||||
### Sheet Format
|
||||
|
||||
Cheatsheets are plain text files that may begin with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
syntax: bash
|
||||
tags: [networking, linux, ssh]
|
||||
---
|
||||
# Connect to remote server
|
||||
ssh user@hostname
|
||||
|
||||
# Copy files over SSH
|
||||
scp local_file user@hostname:/remote/path
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
make test # Run all tests
|
||||
make coverage # Generate coverage report
|
||||
go test ./... # Go test directly
|
||||
```
|
||||
|
||||
Test files follow Go conventions:
|
||||
- `*_test.go` files in same package
|
||||
- Table-driven tests for multiple scenarios
|
||||
- Mock data in `internal/mock` package
|
||||
|
||||
## Error Handling
|
||||
|
||||
The codebase follows consistent error handling patterns:
|
||||
- Functions return explicit errors
|
||||
- Errors are wrapped with context using `fmt.Errorf`
|
||||
- User-facing errors are written to stderr
|
||||
|
||||
Example:
|
||||
```go
|
||||
sheet, err := sheet.New(path, tags, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load sheet: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Developing with Docker
|
||||
|
||||
It may be useful to test your changes within a pristine environment. An Alpine-based docker container has been provided for that purpose.
|
||||
|
||||
Build the docker container:
|
||||
```bash
|
||||
If you would like to build the docker container, run:
|
||||
```sh
|
||||
make docker-setup
|
||||
```
|
||||
|
||||
Shell into the container:
|
||||
```bash
|
||||
To shell into the container, run:
|
||||
```sh
|
||||
make docker-sh
|
||||
```
|
||||
|
||||
The `cheat` source code will be mounted at `/app` within the container.
|
||||
|
||||
To destroy the container:
|
||||
```bash
|
||||
If you would like to destroy this container, you may run:
|
||||
```sh
|
||||
make distclean
|
||||
```
|
||||
|
||||
[go]: https://go.dev/
|
||||
|
||||
@@ -9,20 +9,20 @@ On Unix-like systems, you may simply paste the following snippet into your termi
|
||||
|
||||
```sh
|
||||
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.4.2/cheat-linux-amd64.gz \
|
||||
&& gunzip cheat-linux-amd64.gz \
|
||||
&& chmod +x cheat-linux-amd64 \
|
||||
&& 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.4.2`) and the archive
|
||||
(`cheat-linux-amd64.gz`) depending on your platform.
|
||||
|
||||
See the [releases page][releases] for a list of supported platforms.
|
||||
|
||||
#### Windows
|
||||
On Windows, download the appropriate binary from the [releases page][releases],
|
||||
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
||||
TODO: community support is requested here. Please open a PR if you'd like to
|
||||
contribute installation instructions for Windows.
|
||||
|
||||
### Install via `go install`
|
||||
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
||||
|
||||
113
Makefile
113
Makefile
@@ -3,9 +3,6 @@ makefile := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
cmd_dir := ./cmd/cheat
|
||||
dist_dir := ./dist
|
||||
|
||||
# parallel jobs for build-release (can be overridden)
|
||||
JOBS ?= 8
|
||||
|
||||
# executables
|
||||
CAT := cat
|
||||
COLUMN := column
|
||||
@@ -34,7 +31,6 @@ TMPDIR := /tmp
|
||||
# release binaries
|
||||
releases := \
|
||||
$(dist_dir)/cheat-darwin-amd64 \
|
||||
$(dist_dir)/cheat-darwin-arm64 \
|
||||
$(dist_dir)/cheat-linux-386 \
|
||||
$(dist_dir)/cheat-linux-amd64 \
|
||||
$(dist_dir)/cheat-linux-arm5 \
|
||||
@@ -48,78 +44,70 @@ releases := \
|
||||
|
||||
## build: build an executable for your architecture
|
||||
.PHONY: build
|
||||
build: | clean $(dist_dir) fmt lint vet vendor man
|
||||
build: | clean $(dist_dir) generate fmt lint vet vendor man
|
||||
$(GO) build $(BUILD_FLAGS) -o $(dist_dir)/cheat $(cmd_dir)
|
||||
|
||||
## build-release: build release executables
|
||||
# Runs prepare once, then builds all binaries in parallel
|
||||
# Override jobs with: make build-release JOBS=16
|
||||
.PHONY: build-release
|
||||
build-release: prepare
|
||||
$(MAKE) -j$(JOBS) $(releases)
|
||||
build-release: $(releases)
|
||||
|
||||
# cheat-darwin-amd64
|
||||
$(dist_dir)/cheat-darwin-amd64:
|
||||
$(dist_dir)/cheat-darwin-amd64: prepare
|
||||
GOARCH=amd64 GOOS=darwin \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-darwin-arm64
|
||||
$(dist_dir)/cheat-darwin-arm64:
|
||||
GOARCH=arm64 GOOS=darwin \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-386
|
||||
$(dist_dir)/cheat-linux-386:
|
||||
$(dist_dir)/cheat-linux-386: prepare
|
||||
GOARCH=386 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-amd64
|
||||
$(dist_dir)/cheat-linux-amd64:
|
||||
$(dist_dir)/cheat-linux-amd64: prepare
|
||||
GOARCH=amd64 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm5
|
||||
$(dist_dir)/cheat-linux-arm5:
|
||||
$(dist_dir)/cheat-linux-arm5: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=5 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm6
|
||||
$(dist_dir)/cheat-linux-arm6:
|
||||
$(dist_dir)/cheat-linux-arm6: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=6 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm7
|
||||
$(dist_dir)/cheat-linux-arm7:
|
||||
$(dist_dir)/cheat-linux-arm7: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=7 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm64
|
||||
$(dist_dir)/cheat-linux-arm64:
|
||||
$(dist_dir)/cheat-linux-arm64: prepare
|
||||
GOARCH=arm64 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-netbsd-amd64
|
||||
$(dist_dir)/cheat-netbsd-amd64:
|
||||
$(dist_dir)/cheat-netbsd-amd64: prepare
|
||||
GOARCH=amd64 GOOS=netbsd \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-openbsd-amd64
|
||||
$(dist_dir)/cheat-openbsd-amd64:
|
||||
$(dist_dir)/cheat-openbsd-amd64: prepare
|
||||
GOARCH=amd64 GOOS=openbsd \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-plan9-amd64
|
||||
$(dist_dir)/cheat-plan9-amd64:
|
||||
$(dist_dir)/cheat-plan9-amd64: prepare
|
||||
GOARCH=amd64 GOOS=plan9 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-solaris-amd64
|
||||
$(dist_dir)/cheat-solaris-amd64:
|
||||
$(dist_dir)/cheat-solaris-amd64: prepare
|
||||
GOARCH=amd64 GOOS=solaris \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-windows-amd64
|
||||
$(dist_dir)/cheat-windows-amd64.exe:
|
||||
$(dist_dir)/cheat-windows-amd64.exe: prepare
|
||||
GOARCH=amd64 GOOS=windows \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@ -j
|
||||
|
||||
@@ -127,9 +115,9 @@ $(dist_dir)/cheat-windows-amd64.exe:
|
||||
$(dist_dir):
|
||||
$(MKDIR) $(dist_dir)
|
||||
|
||||
# .tmp
|
||||
.tmp:
|
||||
$(MKDIR) .tmp
|
||||
.PHONY: generate
|
||||
generate:
|
||||
$(GO) generate $(cmd_dir)
|
||||
|
||||
## install: build and install cheat on your PATH
|
||||
.PHONY: install
|
||||
@@ -139,8 +127,7 @@ install: build
|
||||
## clean: remove compiled executables
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) -f $(dist_dir)/*
|
||||
$(RM) -rf .tmp
|
||||
$(RM) -f $(dist_dir)/* $(cmd_dir)/str_config.go $(cmd_dir)/str_usage.go
|
||||
|
||||
## distclean: remove the tags file
|
||||
.PHONY: distclean
|
||||
@@ -151,8 +138,7 @@ distclean:
|
||||
## setup: install revive (linter) and scc (sloc tool)
|
||||
.PHONY: setup
|
||||
setup:
|
||||
$(GO) install github.com/boyter/scc@latest
|
||||
$(GO) install github.com/mgechev/revive@latest
|
||||
GO111MODULE=off $(GO) get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
|
||||
## sloc: count "semantic lines of code"
|
||||
.PHONY: sloc
|
||||
@@ -176,7 +162,6 @@ vendor:
|
||||
$(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
|
||||
|
||||
## vendor-update: update vendored dependencies
|
||||
.PHONY: vendor-update
|
||||
vendor-update:
|
||||
$(GO) get -t -u ./... && $(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
|
||||
|
||||
@@ -200,70 +185,18 @@ vet:
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
## test-integration: run integration tests (requires network)
|
||||
.PHONY: test-integration
|
||||
test-integration:
|
||||
$(GO) test -tags=integration -count=1 ./...
|
||||
|
||||
## test-all: run all tests (unit and integration)
|
||||
.PHONY: test-all
|
||||
test-all: test test-integration
|
||||
|
||||
## test-fuzz: run quick fuzz tests for security-critical functions
|
||||
.PHONY: test-fuzz
|
||||
test-fuzz:
|
||||
@./build/fuzz.sh 15s
|
||||
|
||||
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
||||
.PHONY: test-fuzz-long
|
||||
test-fuzz-long:
|
||||
@./build/fuzz.sh 10m
|
||||
|
||||
## coverage: generate a test coverage report
|
||||
.PHONY: coverage
|
||||
coverage: .tmp
|
||||
$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
|
||||
$(GO) tool cover -html=.tmp/cheat-coverage.out -o .tmp/cheat-coverage.html && \
|
||||
echo "Coverage report generated: .tmp/cheat-coverage.html" && \
|
||||
(sensible-browser .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
xdg-open .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
open .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
echo "Please open .tmp/cheat-coverage.html in your browser")
|
||||
|
||||
## coverage-text: show test coverage by function in terminal
|
||||
.PHONY: coverage-text
|
||||
coverage-text: .tmp
|
||||
$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
|
||||
$(GO) tool cover -func=.tmp/cheat-coverage.out | $(SORT) -k3 -n
|
||||
|
||||
## benchmark: run performance benchmarks
|
||||
.PHONY: benchmark
|
||||
benchmark: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
|
||||
$(RM) -f cheat.test
|
||||
|
||||
## benchmark-cpu: run benchmarks with CPU profiling
|
||||
.PHONY: benchmark-cpu
|
||||
benchmark-cpu: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
|
||||
$(RM) -f cheat.test && \
|
||||
echo "CPU profile saved to .tmp/cpu.prof" && \
|
||||
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
||||
|
||||
## benchmark-mem: run benchmarks with memory profiling
|
||||
.PHONY: benchmark-mem
|
||||
benchmark-mem: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
|
||||
$(RM) -f cheat.test && \
|
||||
echo "Memory profile saved to .tmp/mem.prof" && \
|
||||
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
||||
coverage:
|
||||
$(GO) test ./... -coverprofile=$(TMPDIR)/cheat-coverage.out && \
|
||||
$(GO) tool cover -html=$(TMPDIR)/cheat-coverage.out
|
||||
|
||||
## check: format, lint, vet, vendor, and run unit-tests
|
||||
.PHONY: check
|
||||
check: | vendor fmt lint vet test
|
||||
|
||||
.PHONY: prepare
|
||||
prepare: | clean $(dist_dir) vendor fmt lint vet test
|
||||
prepare: | clean $(dist_dir) generate vendor fmt lint vet test
|
||||
|
||||
## docker-setup: create a docker image for use during development
|
||||
.PHONY: docker-setup
|
||||
|
||||
@@ -117,7 +117,7 @@ cheat tar # file is named "tar"
|
||||
cheat foo/bar # file is named "bar", in a "foo" subdirectory
|
||||
```
|
||||
|
||||
Cheatsheet text may optionally be preceded by a YAML frontmatter header that
|
||||
Cheatsheet text may optionally be preceeded by a YAML frontmatter header that
|
||||
assigns tags and specifies syntax:
|
||||
|
||||
```
|
||||
|
||||
92
build/embed.go
Normal file
92
build/embed.go
Normal file
@@ -0,0 +1,92 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// This script embeds `docopt.txt and `conf.yml` into the binary during at
|
||||
// build time.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// get the cwd
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// get the project root
|
||||
root, err := filepath.Abs(cwd + "../../../")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// specify template file information
|
||||
type file struct {
|
||||
In string
|
||||
Out string
|
||||
Method string
|
||||
}
|
||||
|
||||
// enumerate the template files to process
|
||||
files := []file{
|
||||
file{
|
||||
In: "cmd/cheat/docopt.txt",
|
||||
Out: "cmd/cheat/str_usage.go",
|
||||
Method: "usage"},
|
||||
file{
|
||||
In: "configs/conf.yml",
|
||||
Out: "cmd/cheat/str_config.go",
|
||||
Method: "configs"},
|
||||
}
|
||||
|
||||
// iterate over each static file
|
||||
for _, file := range files {
|
||||
|
||||
// delete the outfile
|
||||
os.Remove(filepath.Join(root, file.Out))
|
||||
|
||||
// read the static template
|
||||
bytes, err := ioutil.ReadFile(filepath.Join(root, file.In))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// render the template
|
||||
data := template(file.Method, string(bytes))
|
||||
|
||||
// write the file to the specified outpath
|
||||
spath := filepath.Join(root, file.Out)
|
||||
err = ioutil.WriteFile(spath, []byte(data), 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// template packages the
|
||||
func template(method string, body string) string {
|
||||
|
||||
// specify the template string
|
||||
t := `package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func %s() string {
|
||||
return strings.TrimSpace(%s)
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(t, method, "`"+body+"`")
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Run fuzz tests for cheat
|
||||
# Usage: ./scripts/fuzz.sh [duration]
|
||||
#
|
||||
# Note: Go's fuzzer will fail immediately if it finds a known failing input
|
||||
# in the corpus (testdata/fuzz/*). This is by design - it ensures you fix
|
||||
# known bugs before searching for new ones. To see failing inputs:
|
||||
# ls internal/*/testdata/fuzz/*/
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
DURATION="${1:-15s}"
|
||||
|
||||
# Define fuzz tests: "TestName:Package:Description"
|
||||
TESTS=(
|
||||
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
||||
"FuzzValidateSheetName:./internal/cheatpath:sheet name validation (path traversal protection)"
|
||||
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
||||
"FuzzSearchCatastrophicBacktracking:./internal/sheet:catastrophic backtracking"
|
||||
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
||||
"FuzzFilter:./internal/sheets:tag filtering operations"
|
||||
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
||||
)
|
||||
|
||||
echo "Running fuzz tests ($DURATION each)..."
|
||||
echo
|
||||
|
||||
for i in "${!TESTS[@]}"; do
|
||||
IFS=':' read -r test_name package description <<< "${TESTS[$i]}"
|
||||
echo "$((i+1)). Testing $description..."
|
||||
go test -fuzz="^${test_name}$" -fuzztime="$DURATION" "$package"
|
||||
echo
|
||||
done
|
||||
|
||||
echo "All fuzz tests passed!"
|
||||
@@ -17,12 +17,6 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--edit"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,22 +5,15 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/cheatpath"
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
// cmdRemove removes (deletes) a cheatsheet.
|
||||
// cmdRemove opens a cheatsheet for editing (or creates it if it doesn't exist).
|
||||
func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--rm"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,21 +31,6 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// prepare the search pattern
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex once, outside the loop
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// iterate over each cheatpath
|
||||
out := ""
|
||||
for _, pathcheats := range cheatsheets {
|
||||
@@ -59,6 +44,21 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
// assume that we want to perform a case-insensitive search for <phrase>
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// `Search` will return text entries that match the search terms.
|
||||
// We're using it here to overwrite the prior cheatsheet Text,
|
||||
// filtering it to only what is relevant.
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
// configs returns the default configuration template
|
||||
func configs() string {
|
||||
return `---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
#
|
||||
# Whenever cheatsheets share the same title (like 'tar'), the most local
|
||||
# cheatsheets (those which come later in this file) take precedence over the
|
||||
# less local sheets. This allows you to create your own "overides" for
|
||||
# "upstream" cheatsheets.
|
||||
#
|
||||
# But what if you want to view the "upstream" cheatsheets instead of your own?
|
||||
# Cheatsheets may be filtered by 'tags' in combination with the '--tag' flag.
|
||||
#
|
||||
# Example: 'cheat tar --tag=community' will display the 'tar' cheatsheet that
|
||||
# is tagged as 'community' rather than your own.
|
||||
#
|
||||
# Paths that come earlier are considered to be the most "global", and paths
|
||||
# that come later are considered to be the most "local". The most "local" paths
|
||||
# take precedence.
|
||||
#
|
||||
# See: https://github.com/cheat/cheat/blob/master/doc/cheat.1.md#cheatpaths
|
||||
cheatpaths:
|
||||
|
||||
# Cheatsheets that are tagged "personal" are stored here by default:
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# Cheatsheets that are tagged "work" are stored here by default:
|
||||
- name: work
|
||||
path: WORK_PATH
|
||||
tags: [ work ]
|
||||
readonly: false
|
||||
|
||||
# Community cheatsheets are stored here by default:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# You can also use glob patterns to automatically load cheatsheets from all
|
||||
# directories that match.
|
||||
#
|
||||
# Example: overload cheatsheets for projects under ~/src/github.com/example/*/
|
||||
#- name: example-projects
|
||||
# path: ~/src/github.com/example/**/.cheat
|
||||
# tags: [ example ]
|
||||
# readonly: true`
|
||||
}
|
||||
59
cmd/cheat/docopt.txt
Normal file
59
cmd/cheat/docopt.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-a --all Search among all cheatpaths
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on cheatpath <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<cheatsheet> Remove (delete) <cheatsheet>
|
||||
--conf Display the config file path
|
||||
|
||||
Examples:
|
||||
|
||||
To initialize a config file:
|
||||
mkdir -p ~/.config/cheat && cheat --init > ~/.config/cheat/conf.yml
|
||||
|
||||
To view the tar cheatsheet:
|
||||
cheat tar
|
||||
|
||||
To edit (or create) the foo cheatsheet:
|
||||
cheat -e foo
|
||||
|
||||
To edit (or create) the foo/bar cheatsheet on the "work" cheatpath:
|
||||
cheat -p work -e foo/bar
|
||||
|
||||
To view all cheatsheet directories:
|
||||
cheat -d
|
||||
|
||||
To list all available cheatsheets:
|
||||
cheat -l
|
||||
|
||||
To list all cheatsheets whose titles match "apt":
|
||||
cheat -l apt
|
||||
|
||||
To list all tags in use:
|
||||
cheat -T
|
||||
|
||||
To list available cheatsheets that are tagged as "personal":
|
||||
cheat -l -t personal
|
||||
|
||||
To search for "ssh" among all cheatsheets, and colorize matches:
|
||||
cheat -c -s ssh
|
||||
|
||||
To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf
|
||||
@@ -1,6 +1,8 @@
|
||||
// Package main serves as the executable entrypoint.
|
||||
package main
|
||||
|
||||
//go:generate go run ../../build/embed.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,7 +17,7 @@ import (
|
||||
"github.com/cheat/cheat/internal/installer"
|
||||
)
|
||||
|
||||
const version = "4.5.0"
|
||||
const version = "4.4.2"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -43,7 +45,6 @@ func main() {
|
||||
// read the envvars into a map of strings
|
||||
envvars := map[string]string{}
|
||||
for _, e := range os.Environ() {
|
||||
// os.Environ() guarantees "key=value" format (see ADR-002)
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
||||
// path traversal attempts when invoked as a subprocess.
|
||||
func TestPathTraversalIntegration(t *testing.T) {
|
||||
// Build the cheat binary
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment
|
||||
testDir := t.TempDir()
|
||||
sheetsDir := filepath.Join(testDir, "sheets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
|
||||
// Create config
|
||||
config := fmt.Sprintf(`---
|
||||
editor: echo
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testDir, "config.yml")
|
||||
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Test table
|
||||
tests := []struct {
|
||||
name string
|
||||
command []string
|
||||
wantFail bool
|
||||
wantMsg string
|
||||
}{
|
||||
// Blocked patterns
|
||||
{
|
||||
name: "block parent traversal edit",
|
||||
command: []string{"--edit", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block absolute path edit",
|
||||
command: []string{"--edit", "/etc/passwd"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be an absolute path",
|
||||
},
|
||||
{
|
||||
name: "block home dir edit",
|
||||
command: []string{"--edit", "~/.ssh/config"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '~'",
|
||||
},
|
||||
{
|
||||
name: "block parent traversal remove",
|
||||
command: []string{"--rm", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block complex traversal",
|
||||
command: []string{"--edit", "foo/../../bar"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block just dots",
|
||||
command: []string{"--edit", ".."},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block empty name",
|
||||
command: []string{"--edit", ""},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be empty",
|
||||
},
|
||||
// Allowed patterns
|
||||
{
|
||||
name: "allow simple name",
|
||||
command: []string{"--edit", "docker"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "allow nested name",
|
||||
command: []string{"--edit", "lang/go"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "block hidden file",
|
||||
command: []string{"--edit", ".gitignore"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "allow current dir",
|
||||
command: []string{"--edit", "./local"},
|
||||
wantFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := exec.Command(binPath, tc.command...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testDir),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if tc.wantFail {
|
||||
if err == nil {
|
||||
t.Errorf("Expected failure but command succeeded. Output: %s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Expected 'invalid cheatsheet name' error, got: %s", output)
|
||||
}
|
||||
if tc.wantMsg != "" && !strings.Contains(string(output), tc.wantMsg) {
|
||||
t.Errorf("Expected message %q in output, got: %s", tc.wantMsg, output)
|
||||
}
|
||||
} else {
|
||||
// Command might fail for other reasons (e.g., editor not found)
|
||||
// but should NOT fail with "invalid cheatsheet name"
|
||||
if strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Command incorrectly blocked. Output: %s", output)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPathTraversalRealWorld tests with more realistic scenarios
|
||||
func TestPathTraversalRealWorld(t *testing.T) {
|
||||
// This test ensures our protection works with actual file operations
|
||||
|
||||
// Build cheat
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
// Create test structure
|
||||
testRoot := t.TempDir()
|
||||
sheetsDir := filepath.Join(testRoot, "cheatsheets")
|
||||
secretDir := filepath.Join(testRoot, "secrets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
os.MkdirAll(secretDir, 0755)
|
||||
|
||||
// Create a "secret" file that should not be accessible
|
||||
secretFile := filepath.Join(secretDir, "secret.txt")
|
||||
os.WriteFile(secretFile, []byte("SECRET DATA"), 0644)
|
||||
|
||||
// Create config using vim in non-interactive mode
|
||||
config := fmt.Sprintf(`---
|
||||
editor: vim -u NONE -n --cmd "set noswapfile" --cmd "wq"
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testRoot, "config.yml")
|
||||
os.WriteFile(configPath, []byte(config), 0644)
|
||||
|
||||
// Test 1: Try to edit a file outside cheatsheets using traversal
|
||||
cmd := exec.Command(binPath, "--edit", "../secrets/secret")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testRoot),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err == nil || !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Path traversal was not blocked! Output: %s", output)
|
||||
}
|
||||
|
||||
// Test 2: Verify the secret file is still intact
|
||||
content, _ := os.ReadFile(secretFile)
|
||||
if string(content) != "SECRET DATA" {
|
||||
t.Errorf("Secret file was modified!")
|
||||
}
|
||||
|
||||
// Test 3: Verify no files were created outside sheets directory
|
||||
err = filepath.Walk(testRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() &&
|
||||
path != configPath &&
|
||||
path != secretFile &&
|
||||
!strings.HasPrefix(path, sheetsDir) {
|
||||
t.Errorf("File created outside allowed directory: %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Walk error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
||||
func BenchmarkSearchCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
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 {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment in .tmp
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Clone community cheatsheets (or reuse if already exists)
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
b.Logf("Cloning community cheatsheets to %s...", cheatsheetDir)
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a minimal config file
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
b.Fatalf("Failed to create config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
|
||||
b.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Set environment to use our config
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"SimpleSearch", []string{"-s", "echo"}},
|
||||
{"RegexSearch", []string{"-r", "-s", "^#.*example"}},
|
||||
{"ColorizedSearch", []string{"-c", "-s", "grep"}},
|
||||
{"ComplexRegex", []string{"-r", "-s", "(git|hg|svn)\\s+(add|commit|push)"}},
|
||||
{"AllCheatpaths", []string{"-a", "-s", "list"}},
|
||||
}
|
||||
|
||||
// Warm up - run once to ensure everything is loaded
|
||||
warmupCmd := exec.Command(cheatBin, "-l")
|
||||
warmupCmd.Env = env
|
||||
warmupCmd.Run()
|
||||
|
||||
// Run benchmarks
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
// Reset timer to exclude setup
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, tc.args...)
|
||||
cmd.Env = env
|
||||
|
||||
// Capture output to prevent spamming
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
start := time.Now()
|
||||
err := cmd.Run()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
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 {
|
||||
b.Fatal("No output from search")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkListCommand benchmarks the list command for comparison
|
||||
func BenchmarkListCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
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 {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment (simplified - reuse if possible)
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Check if we need to clone
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create config
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
os.MkdirAll(configDir, 0755)
|
||||
os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, "-l")
|
||||
cmd.Env = env
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.Fatalf("Command failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
93
cmd/cheat/str_config.go
Normal file
93
cmd/cheat/str_config.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func configs() string {
|
||||
return strings.TrimSpace(`---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# Cheatpaths are paths at which cheatsheets are available on your local
|
||||
# filesystem.
|
||||
#
|
||||
# It is useful to sort cheatsheets into different cheatpaths for organizational
|
||||
# purposes. For example, you might want one cheatpath for community
|
||||
# cheatsheets, one for personal cheatsheets, one for cheatsheets pertaining to
|
||||
# your day job, one for code snippets, etc.
|
||||
#
|
||||
# Cheatpaths are scoped, such that more "local" cheatpaths take priority over
|
||||
# more "global" cheatpaths. (The most global cheatpath is listed first in this
|
||||
# file; the most local is listed last.) For example, if there is a 'tar'
|
||||
# cheatsheet on both global and local paths, you'll be presented with the local
|
||||
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
|
||||
# cheatpaths.)
|
||||
#
|
||||
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
|
||||
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
|
||||
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
|
||||
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
|
||||
# writeable cheatpath.
|
||||
#
|
||||
# This is very useful when you would like to maintain, for example, a
|
||||
# "pristine" repository of community cheatsheets on one cheatpath, and an
|
||||
# editable personal reponsity of cheatsheets on another cheatpath.
|
||||
#
|
||||
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
|
||||
# on certain paths, which can be useful for querying purposes.
|
||||
# Example: 'cheat -t work jenkins'.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
cheatpaths:
|
||||
# Cheatpath properties mean the following:
|
||||
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
|
||||
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
|
||||
# 'tags': tags that should be automatically applied to sheets on this path
|
||||
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# cheat will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Similarly,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
`)
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package main
|
||||
|
||||
// usage returns the usage text for the cheat command
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func usage() string {
|
||||
return `Usage:
|
||||
return strings.TrimSpace(`Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
@@ -60,5 +65,6 @@ Examples:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf`
|
||||
cheat --conf
|
||||
`)
|
||||
}
|
||||
82
configs/conf.yml
Normal file
82
configs/conf.yml
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# Cheatpaths are paths at which cheatsheets are available on your local
|
||||
# filesystem.
|
||||
#
|
||||
# It is useful to sort cheatsheets into different cheatpaths for organizational
|
||||
# purposes. For example, you might want one cheatpath for community
|
||||
# cheatsheets, one for personal cheatsheets, one for cheatsheets pertaining to
|
||||
# your day job, one for code snippets, etc.
|
||||
#
|
||||
# Cheatpaths are scoped, such that more "local" cheatpaths take priority over
|
||||
# more "global" cheatpaths. (The most global cheatpath is listed first in this
|
||||
# file; the most local is listed last.) For example, if there is a 'tar'
|
||||
# cheatsheet on both global and local paths, you'll be presented with the local
|
||||
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
|
||||
# cheatpaths.)
|
||||
#
|
||||
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
|
||||
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
|
||||
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
|
||||
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
|
||||
# writeable cheatpath.
|
||||
#
|
||||
# This is very useful when you would like to maintain, for example, a
|
||||
# "pristine" repository of community cheatsheets on one cheatpath, and an
|
||||
# editable personal reponsity of cheatsheets on another cheatpath.
|
||||
#
|
||||
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
|
||||
# on certain paths, which can be useful for querying purposes.
|
||||
# Example: 'cheat -t work jenkins'.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
cheatpaths:
|
||||
# Cheatpath properties mean the following:
|
||||
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
|
||||
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
|
||||
# 'tags': tags that should be automatically applied to sheets on this path
|
||||
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# cheat will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Similarly,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
@@ -1,169 +0,0 @@
|
||||
# ADR-001: Path Traversal Protection for Cheatsheet Names
|
||||
|
||||
Date: 2025-01-21
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The `cheat` tool allows users to create, edit, and remove cheatsheets using commands like:
|
||||
- `cheat --edit <name>`
|
||||
- `cheat --rm <name>`
|
||||
|
||||
Without validation, a user could potentially provide malicious names like:
|
||||
- `../../../etc/passwd` (directory traversal)
|
||||
- `/etc/passwd` (absolute path)
|
||||
- `~/.ssh/authorized_keys` (home directory expansion)
|
||||
|
||||
While `cheat` is a local tool run by the user themselves (not a network service), path traversal could still lead to:
|
||||
1. Accidental file overwrites outside cheatsheet directories
|
||||
2. Confusion about where files are being created
|
||||
3. Potential security issues in shared environments
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented input validation for cheatsheet names to prevent directory traversal attacks. The validation rejects names that:
|
||||
|
||||
1. Contain `..` (parent directory references)
|
||||
2. Are absolute paths (start with `/` on Unix)
|
||||
3. Start with `~` (home directory expansion)
|
||||
4. Are empty
|
||||
5. Start with `.` (hidden files - these are not displayed by cheat)
|
||||
|
||||
The validation is performed at the application layer before any file operations occur.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Validation Function
|
||||
|
||||
The validation is implemented in `internal/cheatpath/validate.go`:
|
||||
|
||||
```go
|
||||
func ValidateSheetName(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)
|
||||
filename := filepath.Base(name)
|
||||
if strings.HasPrefix(filename, ".") {
|
||||
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
The validation is called in:
|
||||
- `cmd/cheat/cmd_edit.go` - before creating or editing a cheatsheet
|
||||
- `cmd/cheat/cmd_remove.go` - before removing a cheatsheet
|
||||
|
||||
### Allowed Patterns
|
||||
|
||||
The following patterns are explicitly allowed:
|
||||
- Simple names: `docker`, `git`
|
||||
- Nested paths: `docker/compose`, `lang/go/slice`
|
||||
- Current directory references: `./mysheet`
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Safety**: Prevents accidental or intentional file operations outside cheatsheet directories
|
||||
2. **Simplicity**: Validation happens early, before any file operations
|
||||
3. **User-friendly**: Clear error messages explain why a name was rejected
|
||||
4. **Performance**: Minimal overhead - simple string checks
|
||||
5. **Compatibility**: Doesn't break existing valid cheatsheet names
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Limitation**: Users cannot use `..` in cheatsheet names even if legitimate
|
||||
2. **No symlink support**: Cannot create cheatsheets through symlinks outside the cheatpath
|
||||
|
||||
### Neutral
|
||||
|
||||
1. Uses Go's `filepath.IsAbs()` which handles platform differences (Windows vs Unix)
|
||||
2. No attempt to resolve or canonicalize paths - validation is purely syntactic
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
`cheat` is a local command-line tool, not a network service. The primary threats are:
|
||||
- User error (accidentally overwriting important files)
|
||||
- Malicious scripts that invoke `cheat` with crafted arguments
|
||||
- Shared system scenarios where cheatsheets might be shared
|
||||
|
||||
### What This Protects Against
|
||||
|
||||
- Directory traversal using `../`
|
||||
- Absolute path access to system files
|
||||
- Shell expansion of `~` to home directory
|
||||
- Empty names that might cause unexpected behavior
|
||||
- Hidden files that wouldn't be displayed anyway
|
||||
|
||||
### What This Does NOT Protect Against
|
||||
|
||||
- Users with filesystem permissions can still directly edit any file
|
||||
- Symbolic links within the cheatpath pointing outside
|
||||
- Race conditions (TOCTOU) - though minimal risk for a local tool
|
||||
- Malicious content within cheatsheets themselves
|
||||
|
||||
## Testing
|
||||
|
||||
Comprehensive tests ensure the validation works correctly:
|
||||
|
||||
1. **Unit tests** (`internal/cheatpath/validate_test.go`) verify the validation logic
|
||||
2. **Integration tests** verify the actual binary blocks malicious inputs
|
||||
3. **No system files are accessed** during testing - all tests use isolated directories
|
||||
|
||||
Example test cases:
|
||||
```bash
|
||||
# These are blocked:
|
||||
cheat --edit "../../../etc/passwd"
|
||||
cheat --edit "/etc/passwd"
|
||||
cheat --edit "~/.ssh/config"
|
||||
cheat --rm ".."
|
||||
|
||||
# These are allowed:
|
||||
cheat --edit "docker"
|
||||
cheat --edit "docker/compose"
|
||||
cheat --edit "./local"
|
||||
```
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
1. **Path resolution and verification**: Resolve the final path and check if it's within the cheatpath
|
||||
- Rejected: More complex, potential race conditions, platform-specific edge cases
|
||||
|
||||
2. **Chroot/sandbox**: Run file operations in a restricted environment
|
||||
- Rejected: Overkill for a local tool, platform compatibility issues
|
||||
|
||||
3. **Filename allowlist**: Only allow alphanumeric characters and specific symbols
|
||||
- Rejected: Too restrictive, would break existing cheatsheets with valid special characters
|
||||
|
||||
## References
|
||||
|
||||
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
|
||||
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
|
||||
- Go filepath package documentation: https://pkg.go.dev/path/filepath
|
||||
@@ -1,100 +0,0 @@
|
||||
# ADR-002: No Defensive Checks for Environment Variable Parsing
|
||||
|
||||
Date: 2025-01-21
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
In `cmd/cheat/main.go` lines 47-52, the code parses environment variables assuming they all contain an equals sign:
|
||||
|
||||
```go
|
||||
for _, e := range os.Environ() {
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
}
|
||||
envvars[pair[0]] = pair[1] // Could panic if pair has < 2 elements
|
||||
}
|
||||
```
|
||||
|
||||
If `os.Environ()` returned a string without an equals sign, `strings.SplitN` would return a slice with only one element, causing a panic when accessing `pair[1]`.
|
||||
|
||||
## Decision
|
||||
|
||||
We will **not** add defensive checks for this condition. The current code that assumes all environment strings contain "=" will remain unchanged.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Go Runtime Guarantees
|
||||
|
||||
Go's official documentation guarantees that `os.Environ()` returns environment variables in the form "key=value". This is a documented contract of the Go runtime that has been stable since Go 1.0.
|
||||
|
||||
### Empirical Evidence
|
||||
|
||||
Testing across platforms confirms:
|
||||
- All environment variables returned by `os.Environ()` contain at least one "="
|
||||
- Empty environment variables appear as "KEY=" (with an empty value)
|
||||
- Even Windows special variables like "=C:=C:\path" maintain the format
|
||||
|
||||
### Cost-Benefit Analysis
|
||||
|
||||
Adding defensive code would:
|
||||
- **Cost**: Add complexity and cognitive overhead
|
||||
- **Cost**: Suggest uncertainty about Go's documented behavior
|
||||
- **Cost**: Create dead code that can never execute under normal conditions
|
||||
- **Benefit**: Protect against a theoretical scenario that violates Go's guarantees
|
||||
|
||||
The only scenarios where this could panic are:
|
||||
1. A bug in Go's runtime (extremely unlikely, would affect all Go programs)
|
||||
2. Corrupted OS-level environment (would cause broader system issues)
|
||||
3. Breaking change in future Go version (would break many programs, unlikely)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Simpler, more readable code
|
||||
- Trust in platform guarantees reduces unnecessary defensive programming
|
||||
- No performance overhead from unnecessary checks
|
||||
|
||||
### Negative
|
||||
- Theoretical panic if Go's guarantees are violated
|
||||
|
||||
### Neutral
|
||||
- Follows Go community standards of trusting standard library contracts
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Add Defensive Check
|
||||
```go
|
||||
if len(pair) < 2 {
|
||||
continue // or pair[1] = ""
|
||||
}
|
||||
```
|
||||
**Rejected**: Adds complexity for a condition that should never occur.
|
||||
|
||||
### 2. Add Panic with Clear Message
|
||||
```go
|
||||
if len(pair) < 2 {
|
||||
panic("os.Environ() contract violation: " + e)
|
||||
}
|
||||
```
|
||||
**Rejected**: Would crash the program for the same theoretical issue.
|
||||
|
||||
### 3. Add Comment Documenting Assumption
|
||||
```go
|
||||
// os.Environ() guarantees "key=value" format, so pair[1] is safe
|
||||
envvars[pair[0]] = pair[1]
|
||||
```
|
||||
**Rejected**: While documentation is good, this particular guarantee is fundamental to Go.
|
||||
|
||||
## Notes
|
||||
|
||||
If Go ever changes this behavior (extremely unlikely as it would break compatibility), it would be caught immediately in testing as the program would panic on startup. This would be a clear signal to revisit this decision.
|
||||
|
||||
## References
|
||||
|
||||
- Go os.Environ() documentation: https://pkg.go.dev/os#Environ
|
||||
- Go os.Environ() source code and tests
|
||||
@@ -1,104 +0,0 @@
|
||||
# ADR-003: No Parallelization for Search Operations
|
||||
|
||||
Date: 2025-01-22
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We investigated optimizing cheat's search performance through parallelization. Initial assumptions suggested that I/O operations (reading multiple cheatsheet files) would be the primary bottleneck, making parallel processing beneficial.
|
||||
|
||||
Performance benchmarks were implemented to measure search operations, and a parallel search implementation using goroutines was created and tested.
|
||||
|
||||
## Decision
|
||||
|
||||
We will **not** implement parallel search. The sequential implementation will remain unchanged.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Performance Profile Analysis
|
||||
|
||||
CPU profiling revealed that search performance is dominated by:
|
||||
- **Process creation overhead** (~30% in `os/exec.(*Cmd).Run`)
|
||||
- **System calls** (~30% in `syscall.Syscall6`)
|
||||
- **Process management** (fork, exec, pipe setup)
|
||||
|
||||
The actual search logic (regex matching, file reading) was negligible in the profile, indicating our optimization efforts were targeting the wrong bottleneck.
|
||||
|
||||
### Benchmark Results
|
||||
|
||||
Parallel implementation showed minimal improvements:
|
||||
- Simple search: 17ms → 15.3ms (10% improvement)
|
||||
- Regex search: 15ms → 14.9ms (minimal improvement)
|
||||
- Colorized search: 19.5ms → 16.8ms (14% improvement)
|
||||
- Complex regex: 20ms → 15.3ms (24% improvement)
|
||||
|
||||
The best case saved only ~5ms in absolute terms.
|
||||
|
||||
### Cost-Benefit Analysis
|
||||
|
||||
**Costs of parallelization:**
|
||||
- Added complexity with goroutines, channels, and synchronization
|
||||
- Increased maintenance burden
|
||||
- More difficult debugging and testing
|
||||
- Potential race conditions
|
||||
|
||||
**Benefits:**
|
||||
- 5-15% performance improvement (5ms in real terms)
|
||||
- Imperceptible to users in interactive use
|
||||
|
||||
### User Experience Perspective
|
||||
|
||||
For a command-line tool:
|
||||
- Current 15-20ms response time is excellent
|
||||
- Users cannot perceive 5ms differences
|
||||
- Sub-50ms is considered "instant" in HCI research
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Simpler, more maintainable codebase
|
||||
- Easier to debug and reason about
|
||||
- No synchronization bugs or race conditions
|
||||
- Focus remains on code clarity
|
||||
|
||||
### Negative
|
||||
- Missed opportunity for ~5ms performance gain
|
||||
- Search remains single-threaded
|
||||
|
||||
### Neutral
|
||||
- Performance remains excellent for intended use case
|
||||
- Follows Go philosophy of preferring simplicity
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Keep Parallel Implementation
|
||||
**Rejected**: Complexity outweighs negligible performance gains.
|
||||
|
||||
### 2. Optimize Process Startup
|
||||
**Rejected**: Process creation overhead is inherent to CLI tools and cannot be avoided without fundamental architecture changes.
|
||||
|
||||
### 3. Future Optimizations
|
||||
If performance becomes critical, consider:
|
||||
- **Long-running daemon**: Eliminate process startup overhead entirely
|
||||
- **Shell function**: Reduce fork/exec overhead
|
||||
- **Compiled-in cheatsheets**: Eliminate file I/O
|
||||
|
||||
However, these would fundamentally change the tool's architecture and usage model.
|
||||
|
||||
## Notes
|
||||
|
||||
This decision reinforces important principles:
|
||||
1. Always profile before optimizing
|
||||
2. Consider the full execution context
|
||||
3. Measure what matters to users
|
||||
4. Complexity has a real cost
|
||||
|
||||
The parallelization attempt was valuable as a learning exercise and definitively answered whether this optimization path was worthwhile.
|
||||
|
||||
## References
|
||||
|
||||
- Benchmark implementation: cmd/cheat/search_bench_test.go
|
||||
- Reverted parallel implementation: see git history (commit 82eb918)
|
||||
@@ -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
|
||||
98
doc/cheat.1
98
doc/cheat.1
@@ -1,14 +1,31 @@
|
||||
.\" Automatically generated by Pandoc 3.1.11.1
|
||||
.\" Automatically generated by Pandoc 2.17.1.1
|
||||
.\"
|
||||
.\" Define V font for inline verbatim, using C font in formats
|
||||
.\" that render this, and otherwise B font.
|
||||
.ie "\f[CB]x\f[]"x" \{\
|
||||
. ftr V B
|
||||
. ftr VI BI
|
||||
. ftr VB B
|
||||
. ftr VBI BI
|
||||
.\}
|
||||
.el \{\
|
||||
. ftr V CR
|
||||
. ftr VI CI
|
||||
. ftr VB CB
|
||||
. ftr VBI CBI
|
||||
.\}
|
||||
.TH "CHEAT" "1" "" "" "General Commands Manual"
|
||||
.hy
|
||||
.SH NAME
|
||||
\f[B]cheat\f[R] \[em] create and view command\-line cheatsheets
|
||||
.PP
|
||||
\f[B]cheat\f[R] \[em] create and view command-line cheatsheets
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\f[B]cheat\f[R] [options] [\f[I]CHEATSHEET\f[R]]
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
\f[B]cheat\f[R] allows you to create and view interactive cheatsheets on
|
||||
the command\-line.
|
||||
the command-line.
|
||||
It was designed to help remind *nix system administrators of options for
|
||||
commands that they use frequently, but not frequently enough to
|
||||
remember.
|
||||
@@ -17,40 +34,34 @@ remember.
|
||||
\[en]init
|
||||
Print a config file to stdout.
|
||||
.TP
|
||||
\[en]conf
|
||||
Display the config file path.
|
||||
.TP
|
||||
\-a, \[en]all
|
||||
Search among all cheatpaths.
|
||||
.TP
|
||||
\-c, \[en]colorize
|
||||
-c, \[en]colorize
|
||||
Colorize output.
|
||||
.TP
|
||||
\-d, \[en]directories
|
||||
-d, \[en]directories
|
||||
List cheatsheet directories.
|
||||
.TP
|
||||
\-e, \[en]edit=\f[I]CHEATSHEET\f[R]
|
||||
-e, \[en]edit=\f[I]CHEATSHEET\f[R]
|
||||
Open \f[I]CHEATSHEET\f[R] for editing.
|
||||
.TP
|
||||
\-l, \[en]list
|
||||
-l, \[en]list
|
||||
List available cheatsheets.
|
||||
.TP
|
||||
\-p, \[en]path=\f[I]PATH\f[R]
|
||||
-p, \[en]path=\f[I]PATH\f[R]
|
||||
Filter only to sheets found on path \f[I]PATH\f[R].
|
||||
.TP
|
||||
\-r, \[en]regex
|
||||
-r, \[en]regex
|
||||
Treat search \f[I]PHRASE\f[R] as a regular expression.
|
||||
.TP
|
||||
\-s, \[en]search=\f[I]PHRASE\f[R]
|
||||
-s, \[en]search=\f[I]PHRASE\f[R]
|
||||
Search cheatsheets for \f[I]PHRASE\f[R].
|
||||
.TP
|
||||
\-t, \[en]tag=\f[I]TAG\f[R]
|
||||
-t, \[en]tag=\f[I]TAG\f[R]
|
||||
Filter only to sheets tagged with \f[I]TAG\f[R].
|
||||
.TP
|
||||
\-T, \[en]tags
|
||||
-T, \[en]tags
|
||||
List all tags in use.
|
||||
.TP
|
||||
\-v, \[en]version
|
||||
-v, \[en]version
|
||||
Print the version number.
|
||||
.TP
|
||||
\[en]rm=\f[I]CHEATSHEET\f[R]
|
||||
@@ -61,39 +72,37 @@ To view the foo cheatsheet:
|
||||
cheat \f[I]foo\f[R]
|
||||
.TP
|
||||
To edit (or create) the foo cheatsheet:
|
||||
cheat \-e \f[I]foo\f[R]
|
||||
cheat -e \f[I]foo\f[R]
|
||||
.TP
|
||||
To edit (or create) the foo/bar cheatsheet on the `work' cheatpath:
|
||||
cheat \-p \f[I]work\f[R] \-e \f[I]foo/bar\f[R]
|
||||
cheat -p \f[I]work\f[R] -e \f[I]foo/bar\f[R]
|
||||
.TP
|
||||
To view all cheatsheet directories:
|
||||
cheat \-d
|
||||
cheat -d
|
||||
.TP
|
||||
To list all available cheatsheets:
|
||||
cheat \-l
|
||||
cheat -l
|
||||
.TP
|
||||
To list all cheatsheets whose titles match `apt':
|
||||
cheat \-l \f[I]apt\f[R]
|
||||
cheat -l \f[I]apt\f[R]
|
||||
.TP
|
||||
To list all tags in use:
|
||||
cheat \-T
|
||||
cheat -T
|
||||
.TP
|
||||
To list available cheatsheets that are tagged as `personal':
|
||||
cheat \-l \-t \f[I]personal\f[R]
|
||||
cheat -l -t \f[I]personal\f[R]
|
||||
.TP
|
||||
To search for `ssh' among all cheatsheets, and colorize matches:
|
||||
cheat \-c \-s \f[I]ssh\f[R]
|
||||
cheat -c -s \f[I]ssh\f[R]
|
||||
.TP
|
||||
To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat \-c \-r \-s \f[I]`(?:[0\-9]{1,3}.){3}[0\-9]{1,3}'\f[R]
|
||||
cheat -c -r -s \f[I]`(?:[0-9]{1,3}.){3}[0-9]{1,3}'\f[R]
|
||||
.TP
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat \[en]rm \f[I]foo/bar\f[R]
|
||||
.TP
|
||||
To view the configuration file path:
|
||||
cheat \[en]conf
|
||||
.SH FILES
|
||||
.SS Configuration
|
||||
.PP
|
||||
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
||||
named \f[I]conf.yaml\f[R].
|
||||
\f[B]cheat\f[R] will search for \f[I]conf.yaml\f[R] in varying
|
||||
@@ -124,28 +133,24 @@ Alternatively, you may also generate a config file manually by running
|
||||
\f[B]cheat \[en]init\f[R] and saving its output to the appropriate
|
||||
location for your platform.
|
||||
.SS Cheatpaths
|
||||
.PP
|
||||
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
||||
are the directories in which cheatsheets are stored.
|
||||
Cheatpaths may be configured in \f[I]conf.yaml\f[R], and viewed via
|
||||
\f[B]cheat \-d\f[R].
|
||||
\f[B]cheat -d\f[R].
|
||||
.PP
|
||||
For detailed instructions on how to configure cheatpaths, please refer
|
||||
to the comments in conf.yml.
|
||||
.SS Autocompletion
|
||||
.PP
|
||||
Autocompletion scripts for \f[B]bash\f[R], \f[B]zsh\f[R], and
|
||||
\f[B]fish\f[R] are available for download:
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.bash
|
||||
.UE \c
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.bash>
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.fish
|
||||
.UE \c
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.fish>
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh
|
||||
.UE \c
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh>
|
||||
.PP
|
||||
The \f[B]bash\f[R] and \f[B]zsh\f[R] scripts provide optional
|
||||
integration with \f[B]fzf\f[R], if the latter is available on your
|
||||
@@ -171,12 +176,11 @@ Application error
|
||||
.IP "2." 3
|
||||
Cheatsheet(s) not found
|
||||
.SH BUGS
|
||||
See GitHub issues: \c
|
||||
.UR https://github.com/cheat/cheat/issues
|
||||
.UE \c
|
||||
.PP
|
||||
See GitHub issues: <https://github.com/cheat/cheat/issues>
|
||||
.SH AUTHOR
|
||||
Christopher Allen Lane \c
|
||||
.MT chris@chris-allen-lane.com
|
||||
.ME \c
|
||||
.PP
|
||||
Christopher Allen Lane <chris@chris-allen-lane.com>
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\f[B]fzf(1)\f[R]
|
||||
|
||||
@@ -23,12 +23,6 @@ OPTIONS
|
||||
--init
|
||||
: Print a config file to stdout.
|
||||
|
||||
--conf
|
||||
: Display the config file path.
|
||||
|
||||
-a, --all
|
||||
: Search among all cheatpaths.
|
||||
|
||||
-c, --colorize
|
||||
: Colorize output.
|
||||
|
||||
@@ -99,9 +93,6 @@ To search (by regex) for cheatsheets that contain an IP address:
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
: cheat --rm _foo/bar_
|
||||
|
||||
To view the configuration file path:
|
||||
: cheat --conf
|
||||
|
||||
|
||||
FILES
|
||||
=====
|
||||
|
||||
14
go.mod
14
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
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.12.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
@@ -26,13 +26,13 @@ require (
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
34
go.sum
34
go.sum
@@ -3,8 +3,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
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.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||
@@ -27,14 +27,14 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
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.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
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/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
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/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
@@ -60,15 +60,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
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.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -77,8 +77,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
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=
|
||||
@@ -90,8 +90,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
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/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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=
|
||||
@@ -109,14 +109,14 @@ 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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
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/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
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=
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// management.
|
||||
package cheatpath
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Cheatpath encapsulates cheatsheet path information
|
||||
type Cheatpath struct {
|
||||
Name string `yaml:"name"`
|
||||
@@ -11,18 +9,3 @@ type Cheatpath struct {
|
||||
ReadOnly bool `yaml:"readonly"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheatpathValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cheatpath Cheatpath
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid cheatpath",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "personal",
|
||||
Path: "/home/user/.config/cheat/personal",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "",
|
||||
Path: "/home/user/.config/cheat/personal",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath name cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "personal",
|
||||
Path: "",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "",
|
||||
Path: "",
|
||||
ReadOnly: true,
|
||||
Tags: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath name cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "minimal valid",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "x",
|
||||
Path: "/",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with readonly and tags",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "community",
|
||||
Path: "/usr/share/cheat",
|
||||
ReadOnly: true,
|
||||
Tags: []string{"community", "shared", "readonly"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cheatpath.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Validate() error = %v, want error containing %q", err, tt.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -2,38 +2,16 @@ package cheatpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateSheetName ensures that a cheatsheet name does not contain
|
||||
// directory traversal sequences or other potentially dangerous patterns.
|
||||
func ValidateSheetName(name string) error {
|
||||
// Reject empty names
|
||||
if name == "" {
|
||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||
}
|
||||
// Validate returns an error if the cheatpath is invalid
|
||||
func (c *Cheatpath) Validate() error {
|
||||
|
||||
// Reject names containing directory traversal
|
||||
if strings.Contains(name, "..") {
|
||||
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("invalid cheatpath: name must be specified")
|
||||
}
|
||||
|
||||
// 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)")
|
||||
if c.Path == "" {
|
||||
return fmt.Errorf("invalid cheatpath: path must be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FuzzValidateSheetName tests the ValidateSheetName function with fuzzing
|
||||
// to ensure it properly prevents path traversal and other security issues
|
||||
func FuzzValidateSheetName(f *testing.F) {
|
||||
// Add seed corpus with various valid and malicious inputs
|
||||
// Valid names
|
||||
f.Add("docker")
|
||||
f.Add("docker/compose")
|
||||
f.Add("lang/go/slice")
|
||||
f.Add("my-cheat_sheet")
|
||||
f.Add("file.txt")
|
||||
f.Add("a")
|
||||
f.Add("123")
|
||||
|
||||
// Path traversal attempts
|
||||
f.Add("..")
|
||||
f.Add("../etc/passwd")
|
||||
f.Add("foo/../bar")
|
||||
f.Add("foo/../../etc/passwd")
|
||||
f.Add("..\\windows\\system32")
|
||||
f.Add("foo\\..\\..\\windows")
|
||||
|
||||
// Encoded traversal attempts
|
||||
f.Add("%2e%2e")
|
||||
f.Add("%2e%2e%2f")
|
||||
f.Add("..%2f")
|
||||
f.Add("%2e.")
|
||||
f.Add(".%2e")
|
||||
f.Add("\x2e\x2e")
|
||||
f.Add("\\x2e\\x2e")
|
||||
|
||||
// Unicode and special characters
|
||||
f.Add("€test")
|
||||
f.Add("test€")
|
||||
f.Add("中文")
|
||||
f.Add("🎉emoji")
|
||||
f.Add("\x00null")
|
||||
f.Add("test\x00null")
|
||||
f.Add("\nnewline")
|
||||
f.Add("test\ttab")
|
||||
|
||||
// Absolute paths
|
||||
f.Add("/etc/passwd")
|
||||
f.Add("C:\\Windows\\System32")
|
||||
f.Add("\\\\server\\share")
|
||||
f.Add("//server/share")
|
||||
|
||||
// Home directory
|
||||
f.Add("~")
|
||||
f.Add("~/config")
|
||||
f.Add("~user/file")
|
||||
|
||||
// Hidden files
|
||||
f.Add(".hidden")
|
||||
f.Add("dir/.hidden")
|
||||
f.Add(".git/config")
|
||||
|
||||
// Edge cases
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(" ")
|
||||
f.Add("\t")
|
||||
f.Add(".")
|
||||
f.Add("./")
|
||||
f.Add("./file")
|
||||
f.Add(".../")
|
||||
f.Add("...")
|
||||
f.Add("....")
|
||||
|
||||
// Very long names
|
||||
f.Add(strings.Repeat("a", 255))
|
||||
f.Add(strings.Repeat("a/", 100) + "file")
|
||||
f.Add(strings.Repeat("../", 50) + "etc/passwd")
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
// The function should never panic
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ValidateSheetName panicked with input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := ValidateSheetName(input)
|
||||
|
||||
// Security invariants that must always hold
|
||||
if err == nil {
|
||||
// If validation passed, verify security properties
|
||||
|
||||
// Should not contain ".." for path traversal
|
||||
if strings.Contains(input, "..") {
|
||||
t.Errorf("validation passed but input contains '..': %q", input)
|
||||
}
|
||||
|
||||
// Should not be empty
|
||||
if input == "" {
|
||||
t.Error("validation passed for empty input")
|
||||
}
|
||||
|
||||
// Should not start with ~ (home directory)
|
||||
if strings.HasPrefix(input, "~") {
|
||||
t.Errorf("validation passed but input starts with '~': %q", input)
|
||||
}
|
||||
|
||||
// Base filename should not start with .
|
||||
parts := strings.Split(input, "/")
|
||||
if len(parts) > 0 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if strings.HasPrefix(lastPart, ".") && lastPart != "." {
|
||||
t.Errorf("validation passed but filename starts with '.': %q", input)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: result should be valid UTF-8
|
||||
if !utf8.ValidString(input) {
|
||||
// While the function doesn't explicitly check this,
|
||||
// we want to ensure it handles invalid UTF-8 gracefully
|
||||
t.Logf("validation passed for invalid UTF-8: %q", input)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzValidateSheetNamePathTraversal specifically targets path traversal bypasses
|
||||
func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
||||
// Seed corpus focusing on path traversal variations
|
||||
f.Add("..", "/", "")
|
||||
f.Add("", "..", "/")
|
||||
f.Add("a", "b", "c")
|
||||
|
||||
f.Fuzz(func(t *testing.T, prefix string, middle string, suffix string) {
|
||||
// Construct various path traversal attempts
|
||||
inputs := []string{
|
||||
prefix + ".." + suffix,
|
||||
prefix + "/.." + suffix,
|
||||
prefix + "\\.." + suffix,
|
||||
prefix + middle + ".." + suffix,
|
||||
prefix + "../" + middle + suffix,
|
||||
prefix + "..%2f" + suffix,
|
||||
prefix + "%2e%2e" + suffix,
|
||||
prefix + "%2e%2e%2f" + suffix,
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ValidateSheetName panicked with constructed input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := ValidateSheetName(input)
|
||||
|
||||
// If the input contains literal "..", it must be rejected
|
||||
if strings.Contains(input, "..") && err == nil {
|
||||
t.Errorf("validation incorrectly passed for input containing '..': %q", input)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,106 +1,56 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateSheetName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Valid names
|
||||
{
|
||||
name: "simple name",
|
||||
input: "docker",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with slash",
|
||||
input: "docker/compose",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with multiple slashes",
|
||||
input: "lang/go/slice",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with dash and underscore",
|
||||
input: "my-cheat_sheet",
|
||||
wantErr: false,
|
||||
},
|
||||
// Invalid names
|
||||
{
|
||||
name: "empty name",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "empty",
|
||||
},
|
||||
{
|
||||
name: "parent directory traversal",
|
||||
input: "../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "complex traversal",
|
||||
input: "foo/../../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
input: "/etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "absolute",
|
||||
},
|
||||
{
|
||||
name: "home directory",
|
||||
input: "~/secrets",
|
||||
wantErr: true,
|
||||
errMsg: "'~'",
|
||||
},
|
||||
{
|
||||
name: "just dots",
|
||||
input: "..",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "hidden file not allowed",
|
||||
input: ".hidden",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "current dir is ok",
|
||||
input: "./current",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested hidden file not allowed",
|
||||
input: "config/.gitignore",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
// TestValidateValid asserts that valid cheatpaths validate successfully
|
||||
func TestValidateValid(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Name: "foo",
|
||||
Path: "/foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSheetName(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("ValidateName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err != nil {
|
||||
t.Errorf("failed to validate valid cheatpath: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMissingName asserts that paths that are missing a name fail to
|
||||
// validate
|
||||
func TestValidateMissingName(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Path: "/foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err == nil {
|
||||
t.Errorf("failed to invalidate cheatpath without name")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMissingPath asserts that paths that are missing a path fail to
|
||||
// validate
|
||||
func TestValidateMissingPath(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Name: "foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err == nil {
|
||||
t.Errorf("failed to invalidate cheatpath without path")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,6 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
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 == "" {
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/mock"
|
||||
)
|
||||
|
||||
// TestConfigYAMLErrors tests YAML parsing errors
|
||||
func TestConfigYAMLErrors(t *testing.T) {
|
||||
// Create a temporary file with invalid YAML
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
invalidYAML := filepath.Join(tempDir, "invalid.yml")
|
||||
err = os.WriteFile(invalidYAML, []byte("invalid: yaml: content:\n - no closing"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write invalid yaml: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to load invalid YAML
|
||||
_, err = New(map[string]interface{}{}, invalidYAML, false)
|
||||
if err == 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
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
// Load empty 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 defaults
|
||||
if conf.Style != "bw" {
|
||||
t.Errorf("expected default style 'bw', got %s", conf.Style)
|
||||
}
|
||||
|
||||
if conf.Formatter != "terminal" {
|
||||
t.Errorf("expected default formatter 'terminal', got %s", conf.Formatter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigSymlinkResolution tests symlink resolution
|
||||
func TestConfigSymlinkResolution(t *testing.T) {
|
||||
// Create temp directory structure
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create target directory
|
||||
targetDir := filepath.Join(tempDir, "target")
|
||||
err = os.Mkdir(targetDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create target dir: %v", err)
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
linkPath := filepath.Join(tempDir, "link")
|
||||
err = os.Symlink(targetDir, linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
// Create config with symlink path
|
||||
configContent := `---
|
||||
editor: vim
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: ` + linkPath + `
|
||||
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 with symlink resolution
|
||||
conf, err := New(map[string]interface{}{}, configFile, true)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Verify symlink was resolved
|
||||
if len(conf.Cheatpaths) > 0 && conf.Cheatpaths[0].Path != targetDir {
|
||||
t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigBrokenSymlink tests broken symlink handling
|
||||
func TestConfigBrokenSymlink(t *testing.T) {
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create broken symlink
|
||||
linkPath := filepath.Join(tempDir, "broken-link")
|
||||
err = os.Symlink("/nonexistent/path", linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
// Create config with broken symlink
|
||||
configContent := `---
|
||||
editor: vim
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: ` + linkPath + `
|
||||
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 with symlink resolution should fail
|
||||
_, err = New(map[string]interface{}{}, configFile, true)
|
||||
if err == nil {
|
||||
t.Error("expected error for broken symlink, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,95 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEditor tests the Editor function
|
||||
func TestEditor(t *testing.T) {
|
||||
// Save original env vars
|
||||
oldVisual := os.Getenv("VISUAL")
|
||||
oldEditor := os.Getenv("EDITOR")
|
||||
defer func() {
|
||||
os.Setenv("VISUAL", oldVisual)
|
||||
os.Setenv("EDITOR", oldEditor)
|
||||
}()
|
||||
|
||||
t.Run("windows default", func(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping windows test on non-windows platform")
|
||||
}
|
||||
|
||||
// Clear env vars
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "notepad" {
|
||||
t.Errorf("expected 'notepad' on windows, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("VISUAL takes precedence", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("VISUAL", "emacs")
|
||||
os.Setenv("EDITOR", "nano")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "emacs" {
|
||||
t.Errorf("expected VISUAL to take precedence, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EDITOR when no VISUAL", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "vim")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "vim" {
|
||||
t.Errorf("expected EDITOR value, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no editor found error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
// Clear all environment variables
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "")
|
||||
|
||||
// Create a custom PATH that doesn't include common editors
|
||||
oldPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", oldPath)
|
||||
|
||||
// Set a very limited PATH that won't have editors
|
||||
os.Setenv("PATH", "/nonexistent")
|
||||
|
||||
editor, err := Editor()
|
||||
|
||||
// If we found an editor, it's likely in the system
|
||||
// This test might not always produce an error on systems with editors
|
||||
if editor == "" && err == nil {
|
||||
t.Error("expected error when no editor found")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -37,84 +35,3 @@ func TestInit(t *testing.T) {
|
||||
t.Errorf("failed to write configs: want: %s, got: %s", conf, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitCreateDirectory tests that Init creates the directory if it doesn't exist
|
||||
func TestInitCreateDirectory(t *testing.T) {
|
||||
// Create a temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-init-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Path to a config file in a non-existent subdirectory
|
||||
confPath := filepath.Join(tempDir, "subdir", "conf.yml")
|
||||
|
||||
// Initialize the config file
|
||||
conf := "test config"
|
||||
if err = Init(confPath, conf); err != nil {
|
||||
t.Errorf("failed to init config file: %v", err)
|
||||
}
|
||||
|
||||
// Verify the directory was created
|
||||
if _, err := os.Stat(filepath.Dir(confPath)); os.IsNotExist(err) {
|
||||
t.Error("Init did not create the directory")
|
||||
}
|
||||
|
||||
// Verify the file was created with correct content
|
||||
bytes, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
if string(bytes) != conf {
|
||||
t.Errorf("config content mismatch: got %q, want %q", string(bytes), conf)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitWriteError tests error handling when file write fails
|
||||
func TestInitWriteError(t *testing.T) {
|
||||
// Skip this test if running as root (can write anywhere)
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("Cannot test write errors as root")
|
||||
}
|
||||
|
||||
// Try to write to a read-only directory
|
||||
err := Init("/dev/null/impossible/path/conf.yml", "test")
|
||||
if err == 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
|
||||
func TestInitExistingFile(t *testing.T) {
|
||||
// Create a temp file
|
||||
tempFile, err := os.CreateTemp("", "cheat-init-existing-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write initial content
|
||||
initialContent := "initial content"
|
||||
if err := os.WriteFile(tempFile.Name(), []byte(initialContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write initial content: %v", err)
|
||||
}
|
||||
|
||||
// Initialize with new content
|
||||
newContent := "new config content"
|
||||
if err = Init(tempFile.Name(), newContent); err != nil {
|
||||
t.Errorf("failed to init over existing file: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was overwritten
|
||||
bytes, err := os.ReadFile(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
if string(bytes) != newContent {
|
||||
t.Errorf("config not overwritten: got %q, want %q", string(bytes), newContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTrimsWhitespace(t *testing.T) {
|
||||
// Create a temporary config file with whitespace in editor and pager
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: " vim -c 'set number' "
|
||||
pager: " less -R "
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Verify editor is trimmed
|
||||
expectedEditor := "vim -c 'set number'"
|
||||
if conf.Editor != expectedEditor {
|
||||
t.Errorf("editor not properly trimmed: got %q, want %q", conf.Editor, expectedEditor)
|
||||
}
|
||||
|
||||
// Verify pager is trimmed
|
||||
expectedPager := "less -R"
|
||||
if conf.Pager != expectedPager {
|
||||
t.Errorf("pager not properly trimmed: got %q, want %q", conf.Pager, expectedPager)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmptyEditorFallback(t *testing.T) {
|
||||
// Skip if required environment variables would interfere
|
||||
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 config with whitespace-only editor
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: " "
|
||||
pager: less
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
// It's OK if this fails due to no editor being found
|
||||
// The important thing is it doesn't panic
|
||||
return
|
||||
}
|
||||
|
||||
// If it succeeded, editor should not be empty (fallback was used)
|
||||
if conf.Editor == "" {
|
||||
t.Error("editor should not be empty after fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWhitespaceOnlyPager(t *testing.T) {
|
||||
// Create a config with whitespace-only pager
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: vim
|
||||
pager: " "
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Pager should be empty after trimming
|
||||
if conf.Pager != "" {
|
||||
t.Errorf("pager should be empty after trimming whitespace: got %q", conf.Pager)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func Pager() string {
|
||||
// Otherwise, search for `pager`, `less`, and `more` on the `$PATH`. If
|
||||
// none are found, return an empty pager.
|
||||
for _, pager := range []string{"pager", "less", "more"} {
|
||||
if path, err := exec.LookPath(pager); err == nil {
|
||||
if path, err := exec.LookPath(pager); err != nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPager tests the Pager function
|
||||
func TestPager(t *testing.T) {
|
||||
// Save original env var
|
||||
oldPager := os.Getenv("PAGER")
|
||||
defer os.Setenv("PAGER", oldPager)
|
||||
|
||||
t.Run("windows default", func(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping windows test on non-windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
pager := Pager()
|
||||
if pager != "more" {
|
||||
t.Errorf("expected 'more' on windows, got %s", pager)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PAGER env var", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "bat")
|
||||
pager := Pager()
|
||||
if pager != "bat" {
|
||||
t.Errorf("expected PAGER env var value, got %s", pager)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback to system pager", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
pager := Pager()
|
||||
|
||||
// Should find one of the fallback pagers or return empty string
|
||||
validPagers := map[string]bool{
|
||||
"": true, // no pager found
|
||||
"pager": true,
|
||||
"less": true,
|
||||
"more": true,
|
||||
}
|
||||
|
||||
// Check if it's a path to one of these
|
||||
found := false
|
||||
for p := range validPagers {
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no pager available", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
|
||||
// Save and modify PATH to ensure no pagers are found
|
||||
oldPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", oldPath)
|
||||
os.Setenv("PATH", "/nonexistent")
|
||||
|
||||
pager := Pager()
|
||||
if pager != "" {
|
||||
t.Errorf("expected empty string when no pager found, got %s", pager)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,11 +19,6 @@ func Write(out string, conf config.Config) {
|
||||
}
|
||||
|
||||
// otherwise, pipe output through the pager
|
||||
writeToPager(out, conf)
|
||||
}
|
||||
|
||||
// writeToPager writes output through a pager command
|
||||
func writeToPager(out string, conf config.Config) {
|
||||
parts := strings.Split(conf.Pager, " ")
|
||||
pager := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// TestWriteToPager tests the writeToPager function
|
||||
func TestWriteToPager(t *testing.T) {
|
||||
// Skip these tests in CI/CD environments where interactive commands might not work
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("Skipping pager tests in CI environment")
|
||||
}
|
||||
|
||||
// Note: We can't easily test os.Exit calls, so we focus on testing writeToPager
|
||||
// which contains the core logic
|
||||
|
||||
t.Run("successful pager execution", func(t *testing.T) {
|
||||
// Save original stdout
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create pipe for capturing output
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Use 'cat' as a simple pager that just outputs input
|
||||
conf := config.Config{
|
||||
Pager: "cat",
|
||||
}
|
||||
|
||||
// This will call os.Exit on error, so we need to be careful
|
||||
// We're using 'cat' which should always succeed
|
||||
input := "Test output\n"
|
||||
|
||||
// Run in a goroutine to avoid blocking
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
writeToPager(input, conf)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for completion or timeout
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
}
|
||||
|
||||
// Close write end and read output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
// Verify output
|
||||
if buf.String() != input {
|
||||
t.Errorf("expected output %q, got %q", input, buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pager with arguments", func(t *testing.T) {
|
||||
// Save original stdout
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create pipe for capturing output
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Use 'cat' with '-A' flag (shows non-printing characters)
|
||||
conf := config.Config{
|
||||
Pager: "cat -A",
|
||||
}
|
||||
|
||||
input := "Test\toutput\n"
|
||||
|
||||
// Run in a goroutine
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
writeToPager(input, conf)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for completion
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
}
|
||||
|
||||
// Close write end and read output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
// cat -A shows tabs as ^I and line endings as $
|
||||
expected := "Test^Ioutput$\n"
|
||||
if buf.String() != expected {
|
||||
t.Errorf("expected output %q, got %q", expected, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestWriteToPagerError tests error handling in writeToPager
|
||||
func TestWriteToPagerError(t *testing.T) {
|
||||
if os.Getenv("TEST_PAGER_ERROR_SUBPROCESS") == "1" {
|
||||
// This is the subprocess - run the actual test
|
||||
conf := config.Config{Pager: "/nonexistent/command"}
|
||||
writeToPager("test", conf)
|
||||
return
|
||||
}
|
||||
|
||||
// Run test in subprocess to handle os.Exit
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestWriteToPagerError$")
|
||||
cmd.Env = append(os.Environ(), "TEST_PAGER_ERROR_SUBPROCESS=1")
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Should exit with error
|
||||
if err == nil {
|
||||
t.Error("expected process to exit with error")
|
||||
}
|
||||
|
||||
// Should contain error message
|
||||
if !strings.Contains(string(output), "failed to write to pager") {
|
||||
t.Errorf("expected error message about pager failure, got %q", string(output))
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrompt(t *testing.T) {
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
input string
|
||||
defaultVal bool
|
||||
want bool
|
||||
wantErr bool
|
||||
wantPrompt string
|
||||
}{
|
||||
{
|
||||
name: "answer yes",
|
||||
prompt: "Continue?",
|
||||
input: "y\n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer yes with uppercase",
|
||||
prompt: "Continue?",
|
||||
input: "Y\n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer yes with spaces",
|
||||
prompt: "Continue?",
|
||||
input: " y \n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer no",
|
||||
prompt: "Continue?",
|
||||
input: "n\n",
|
||||
defaultVal: true,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer no with any text",
|
||||
prompt: "Continue?",
|
||||
input: "anything\n",
|
||||
defaultVal: true,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "empty answer uses default true",
|
||||
prompt: "Continue?",
|
||||
input: "\n",
|
||||
defaultVal: true,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "empty answer uses default false",
|
||||
prompt: "Continue?",
|
||||
input: "\n",
|
||||
defaultVal: false,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "whitespace answer uses default",
|
||||
prompt: "Continue?",
|
||||
input: " \n",
|
||||
defaultVal: true,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a pipe for stdin
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Create a pipe for stdout to capture the prompt
|
||||
rOut, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
|
||||
// Write input to stdin
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, tt.input)
|
||||
}()
|
||||
|
||||
// Call the function
|
||||
got, err := Prompt(tt.prompt, tt.defaultVal)
|
||||
|
||||
// Close stdout write end and read the prompt
|
||||
wOut.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, rOut)
|
||||
|
||||
// Check error
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Prompt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
if got != tt.want {
|
||||
t.Errorf("Prompt() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
// Check that prompt was displayed correctly
|
||||
if buf.String() != tt.wantPrompt {
|
||||
t.Errorf("Prompt display = %q, want %q", buf.String(), tt.wantPrompt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptError(t *testing.T) {
|
||||
// Save original stdin
|
||||
oldStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
}()
|
||||
|
||||
// Create a pipe and close it immediately to simulate read error
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
r.Close()
|
||||
w.Close()
|
||||
|
||||
// This should cause a read error
|
||||
_, err := Prompt("Test?", false)
|
||||
if err == nil {
|
||||
t.Error("expected error when reading from closed stdin, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to parse input") {
|
||||
t.Errorf("expected 'failed to parse input' 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)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "cheat-installer-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configs string
|
||||
confpath string
|
||||
userInput string
|
||||
wantErr bool
|
||||
wantInErr string
|
||||
checkFiles []string
|
||||
dontWantFiles []string
|
||||
}{
|
||||
{
|
||||
name: "user declines community cheatsheets",
|
||||
configs: `---
|
||||
editor: EDITOR_PATH
|
||||
pager: PAGER_PATH
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
`,
|
||||
confpath: filepath.Join(tempDir, "conf1", "conf.yml"),
|
||||
userInput: "n\n",
|
||||
wantErr: false,
|
||||
checkFiles: []string{"conf1/conf.yml"},
|
||||
dontWantFiles: []string{"conf1/cheatsheets/community", "conf1/cheatsheets/personal"},
|
||||
},
|
||||
{
|
||||
name: "user accepts but clone fails",
|
||||
configs: `---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
`,
|
||||
confpath: filepath.Join(tempDir, "conf2", "conf.yml"),
|
||||
userInput: "y\n",
|
||||
wantErr: true,
|
||||
wantInErr: "failed to clone cheatsheets",
|
||||
},
|
||||
{
|
||||
name: "invalid config path",
|
||||
configs: "test",
|
||||
confpath: "/nonexistent/path/conf.yml",
|
||||
userInput: "n\n",
|
||||
wantErr: true,
|
||||
wantInErr: "failed to create config file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create stdin pipe
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Create stdout pipe to suppress output
|
||||
_, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
|
||||
// Write user input
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, tt.userInput)
|
||||
}()
|
||||
|
||||
// Run the installer
|
||||
err := Run(tt.configs, tt.confpath)
|
||||
|
||||
// Close pipes
|
||||
wOut.Close()
|
||||
|
||||
// Check error
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err != nil && tt.wantInErr != "" && !strings.Contains(err.Error(), tt.wantInErr) {
|
||||
t.Errorf("Run() error = %v, want error containing %q", err, tt.wantInErr)
|
||||
}
|
||||
|
||||
// Check created files
|
||||
for _, file := range tt.checkFiles {
|
||||
path := filepath.Join(tempDir, file)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Errorf("expected file %s to exist, but it doesn't", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Check files that shouldn't exist
|
||||
for _, file := range tt.dontWantFiles {
|
||||
path := filepath.Join(tempDir, file)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Errorf("expected file %s to not exist, but it does", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPromptError(t *testing.T) {
|
||||
// Save original stdin
|
||||
oldStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
}()
|
||||
|
||||
// Close stdin to cause prompt error
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
r.Close()
|
||||
w.Close()
|
||||
|
||||
tempDir, _ := os.MkdirTemp("", "cheat-installer-prompt-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
err := Run("test", filepath.Join(tempDir, "conf.yml"))
|
||||
if err == nil {
|
||||
t.Error("expected error when prompt fails, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to prompt") {
|
||||
t.Errorf("expected 'failed to prompt' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStringReplacements(t *testing.T) {
|
||||
// Test that path replacements work correctly
|
||||
configs := `---
|
||||
editor: EDITOR_PATH
|
||||
pager: PAGER_PATH
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
`
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-installer-replace-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
confpath := filepath.Join(tempDir, "conf.yml")
|
||||
confdir := filepath.Dir(confpath)
|
||||
|
||||
// Expected paths
|
||||
expectedCommunity := filepath.Join(confdir, "cheatsheets", "community")
|
||||
expectedPersonal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create stdin pipe with "n" answer
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, "n\n")
|
||||
}()
|
||||
|
||||
// Suppress stdout
|
||||
_, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
defer wOut.Close()
|
||||
|
||||
// Run installer
|
||||
err = Run(configs, confpath)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() failed: %v", err)
|
||||
}
|
||||
|
||||
// Read the created config file
|
||||
content, err := os.ReadFile(confpath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
// Check replacements
|
||||
contentStr := string(content)
|
||||
if strings.Contains(contentStr, "COMMUNITY_PATH") {
|
||||
t.Error("COMMUNITY_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "PERSONAL_PATH") {
|
||||
t.Error("PERSONAL_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "EDITOR_PATH") && !strings.Contains(contentStr, fmt.Sprintf("editor: %s", "")) {
|
||||
t.Error("EDITOR_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "PAGER_PATH") && !strings.Contains(contentStr, fmt.Sprintf("pager: %s", "")) {
|
||||
t.Error("PAGER_PATH was not replaced")
|
||||
}
|
||||
|
||||
// Verify correct paths were used
|
||||
if !strings.Contains(contentStr, expectedCommunity) {
|
||||
t.Errorf("expected community path %q in config", expectedCommunity)
|
||||
}
|
||||
if !strings.Contains(contentStr, expectedPersonal) {
|
||||
t.Errorf("expected personal path %q in config", expectedPersonal)
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// Clone clones the community cheatsheets repository to the specified directory
|
||||
func Clone(dir string) error {
|
||||
// Clone clones the repo available at `url`
|
||||
func Clone(url string) error {
|
||||
|
||||
// clone the community cheatsheets
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
_, err := git.PlainClone(url, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
Progress: os.Stdout,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCloneIntegration performs a real clone operation to verify functionality
|
||||
// Run with: go test -tags=integration ./internal/repo -v -run TestCloneIntegration
|
||||
func TestCloneIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create a temporary directory
|
||||
tmpDir, err := os.MkdirTemp("", "cheat-clone-integration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
destDir := filepath.Join(tmpDir, "cheatsheets")
|
||||
|
||||
t.Logf("Cloning to: %s", destDir)
|
||||
|
||||
// Perform the actual clone
|
||||
err = Clone(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Clone() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the clone succeeded
|
||||
info, err := os.Stat(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("destination directory not created: %v", err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
t.Fatal("destination is not a directory")
|
||||
}
|
||||
|
||||
// Check for .git directory
|
||||
gitDir := filepath.Join(destDir, ".git")
|
||||
if _, err := os.Stat(gitDir); err != nil {
|
||||
t.Error(".git directory not found")
|
||||
}
|
||||
|
||||
// Check for some expected cheatsheets
|
||||
expectedFiles := []string{
|
||||
"bash", // bash cheatsheet should exist
|
||||
"git", // git cheatsheet should exist
|
||||
"ls", // ls cheatsheet should exist
|
||||
}
|
||||
|
||||
foundCount := 0
|
||||
for _, file := range expectedFiles {
|
||||
path := filepath.Join(destDir, file)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
foundCount++
|
||||
}
|
||||
}
|
||||
|
||||
if foundCount < 2 {
|
||||
t.Errorf("expected at least 2 common cheatsheets, found %d", foundCount)
|
||||
}
|
||||
|
||||
t.Log("Clone integration test passed!")
|
||||
|
||||
// Test cloning to existing directory (should fail)
|
||||
err = Clone(destDir)
|
||||
if err == nil {
|
||||
t.Error("expected error when cloning to existing repository, got nil")
|
||||
} else {
|
||||
t.Logf("Expected error when cloning to existing dir: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestClone tests the Clone function
|
||||
func TestClone(t *testing.T) {
|
||||
// This test requires network access, so we'll only test error cases
|
||||
// that don't require actual cloning
|
||||
|
||||
t.Run("clone to read-only directory", func(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("Cannot test read-only directory as root")
|
||||
}
|
||||
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-clone-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a read-only subdirectory
|
||||
readOnlyDir := filepath.Join(tempDir, "readonly")
|
||||
if err := os.Mkdir(readOnlyDir, 0555); err != nil {
|
||||
t.Fatalf("failed to create read-only dir: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to clone to read-only directory
|
||||
targetDir := filepath.Join(readOnlyDir, "cheatsheets")
|
||||
err = Clone(targetDir)
|
||||
|
||||
// Should fail because we can't write to read-only directory
|
||||
if err == nil {
|
||||
t.Error("expected error when cloning to read-only directory, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("clone to invalid path", func(t *testing.T) {
|
||||
// Try to clone to a path with null bytes (invalid on most filesystems)
|
||||
err := Clone("/tmp/invalid\x00path")
|
||||
if err == nil {
|
||||
t.Error("expected error with invalid path, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGitDir(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test directory structure
|
||||
testDirs := []string{
|
||||
filepath.Join(tempDir, ".git"),
|
||||
filepath.Join(tempDir, ".git", "objects"),
|
||||
filepath.Join(tempDir, ".git", "refs"),
|
||||
filepath.Join(tempDir, "regular"),
|
||||
filepath.Join(tempDir, "regular", ".git"),
|
||||
filepath.Join(tempDir, "submodule"),
|
||||
}
|
||||
|
||||
for _, dir := range testDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatalf("failed to create dir %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create test files
|
||||
testFiles := map[string]string{
|
||||
filepath.Join(tempDir, ".gitignore"): "*.tmp\n",
|
||||
filepath.Join(tempDir, ".gitattributes"): "* text=auto\n",
|
||||
filepath.Join(tempDir, "submodule", ".git"): "gitdir: ../.git/modules/submodule\n",
|
||||
filepath.Join(tempDir, "regular", "sheet.txt"): "content\n",
|
||||
}
|
||||
|
||||
for file, content := range testFiles {
|
||||
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to create file %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
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) {
|
||||
// Test with paths that have .git but not as a directory separator
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "file ending with .git",
|
||||
path: "/tmp/myfile.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "directory ending with .git",
|
||||
path: "/tmp/myrepo.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: ".github directory",
|
||||
path: "/tmp/.github/workflows",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "legitimate.git-repo name",
|
||||
path: "/tmp/legitimate.git-repo/file",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GitDir(tt.path)
|
||||
if err != nil {
|
||||
// It's ok if the path doesn't exist for these edge case tests
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitDirPathSeparator(t *testing.T) {
|
||||
// Test that the function correctly uses os.PathSeparator
|
||||
// This is important for cross-platform compatibility
|
||||
|
||||
// Create a path with the wrong separator for the current OS
|
||||
var wrongSep string
|
||||
if os.PathSeparator == '/' {
|
||||
wrongSep = `\`
|
||||
} else {
|
||||
wrongSep = `/`
|
||||
}
|
||||
|
||||
// Path with wrong separator should not be detected as git dir
|
||||
path := fmt.Sprintf("some%spath%s.git%sfile", wrongSep, wrongSep, wrongSep)
|
||||
isGit, err := GitDir(path)
|
||||
|
||||
if err != nil {
|
||||
// Path doesn't exist, which is fine
|
||||
return
|
||||
}
|
||||
|
||||
if isGit {
|
||||
t.Errorf("GitDir() incorrectly detected git dir with wrong path separator")
|
||||
}
|
||||
}
|
||||
@@ -32,29 +32,3 @@ func TestColorize(t *testing.T) {
|
||||
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, 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
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ func (s *Sheet) Copy(dest string) error {
|
||||
// copy file contents
|
||||
_, err = io.Copy(outfile, infile)
|
||||
if err != nil {
|
||||
// Clean up the partially written file on error
|
||||
os.Remove(dest)
|
||||
return fmt.Errorf(
|
||||
"failed to copy file: infile: %s, outfile: %s, err: %v",
|
||||
s.Path,
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCopyErrors tests error cases for the Copy method
|
||||
func TestCopyErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() (*Sheet, string, func())
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "source file does not exist",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a sheet with non-existent path
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: "/non/existent/file.txt",
|
||||
CheatPath: "test",
|
||||
}
|
||||
dest := filepath.Join(os.TempDir(), "copy-test-dest.txt")
|
||||
cleanup := func() {
|
||||
os.Remove(dest)
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to open cheatsheet",
|
||||
},
|
||||
{
|
||||
name: "destination directory creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
src.WriteString("test content")
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a file where we want a directory
|
||||
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
|
||||
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
|
||||
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")
|
||||
|
||||
cleanup := func() {
|
||||
os.Remove(src.Name())
|
||||
os.Remove(blockerFile)
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create directory",
|
||||
},
|
||||
{
|
||||
name: "destination file creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
src.WriteString("test content")
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a directory where we want the file
|
||||
destDir := filepath.Join(os.TempDir(), "copy-test-dir")
|
||||
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
|
||||
t.Fatalf("failed to create dest dir: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
os.Remove(src.Name())
|
||||
os.RemoveAll(destDir)
|
||||
}
|
||||
return sheet, destDir, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create outfile",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sheet, dest, cleanup := tt.setup()
|
||||
defer cleanup()
|
||||
|
||||
err := sheet.Copy(dest)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
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
|
||||
func TestCopyIOError(t *testing.T) {
|
||||
// This is difficult to test without mocking io.Copy
|
||||
// The error case would occur if the source file is modified
|
||||
// or removed after opening but before copying
|
||||
t.Skip("Skipping io.Copy error test - requires file system race condition")
|
||||
}
|
||||
|
||||
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
||||
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 {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(src.Name())
|
||||
|
||||
// Write some content
|
||||
content := "test content for cleanup"
|
||||
if _, err := src.WriteString(content); err != nil {
|
||||
t.Fatalf("failed to write content: %v", err)
|
||||
}
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Destination path
|
||||
dest := filepath.Join(os.TempDir(), "copy-cleanup-test.txt")
|
||||
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 {
|
||||
t.Skip("Cannot change file permissions on this platform")
|
||||
}
|
||||
defer os.Chmod(src.Name(), 0644) // Restore permissions for cleanup
|
||||
|
||||
// Attempt to copy - this should fail during io.Copy
|
||||
err = sheet.Copy(dest)
|
||||
if err == nil {
|
||||
t.Error("Expected Copy to fail with permission error")
|
||||
}
|
||||
|
||||
// Verify the destination file was cleaned up
|
||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||
t.Error("Destination file should have been removed after copy 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
|
||||
@@ -1,54 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
||||
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
|
||||
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
||||
|
||||
// parse the frontmatter
|
||||
fm, text, err := parse(markdown)
|
||||
|
||||
// assert expectations
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse markdown: %v", err)
|
||||
}
|
||||
|
||||
want := "To foo the bar: baz"
|
||||
if text != want {
|
||||
t.Errorf("failed to parse text: want: %s, got: %s", want, text)
|
||||
}
|
||||
|
||||
want = "go"
|
||||
if fm.Syntax != want {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzParse tests the parse function with fuzzing to uncover edge cases
|
||||
// and potential panics in YAML frontmatter parsing
|
||||
func FuzzParse(f *testing.F) {
|
||||
// Add seed corpus with various valid and edge case inputs
|
||||
// Valid frontmatter
|
||||
f.Add("---\nsyntax: go\n---\nContent")
|
||||
f.Add("---\ntags: [a, b]\n---\n")
|
||||
f.Add("---\nsyntax: bash\ntags: [linux, shell]\n---\n#!/bin/bash\necho hello")
|
||||
|
||||
// No frontmatter
|
||||
f.Add("No frontmatter here")
|
||||
f.Add("")
|
||||
f.Add("Just plain text\nwith multiple lines")
|
||||
|
||||
// Edge cases with delimiters
|
||||
f.Add("---")
|
||||
f.Add("---\n")
|
||||
f.Add("---\n---")
|
||||
f.Add("---\n---\n")
|
||||
f.Add("---\n---\n---")
|
||||
f.Add("---\n---\n---\n---")
|
||||
f.Add("------\n------")
|
||||
|
||||
// Invalid YAML
|
||||
f.Add("---\n{invalid yaml\n---\n")
|
||||
f.Add("---\nsyntax: \"unclosed quote\n---\n")
|
||||
f.Add("---\ntags: [a, b,\n---\n")
|
||||
|
||||
// Windows line endings
|
||||
f.Add("---\r\nsyntax: go\r\n---\r\nContent")
|
||||
f.Add("---\r\n---\r\n")
|
||||
|
||||
// Mixed line endings
|
||||
f.Add("---\nsyntax: go\r\n---\nContent")
|
||||
f.Add("---\r\nsyntax: go\n---\r\nContent")
|
||||
|
||||
// Unicode and special characters
|
||||
f.Add("---\ntags: [emoji, 🎉]\n---\n")
|
||||
f.Add("---\nsyntax: 中文\n---\n")
|
||||
f.Add("---\ntags: [\x00, \x01]\n---\n")
|
||||
|
||||
// Very long inputs
|
||||
f.Add("---\ntags: [" + strings.Repeat("a,", 1000) + "a]\n---\n")
|
||||
f.Add("---\n" + strings.Repeat("field: value\n", 1000) + "---\n")
|
||||
|
||||
// Nested structures
|
||||
f.Add("---\ntags:\n - nested\n - list\n---\n")
|
||||
f.Add("---\nmeta:\n author: test\n version: 1.0\n---\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
// The parse function should never panic, regardless of input
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("parse panicked with input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
fm, text, err := parse(input)
|
||||
|
||||
// Verify invariants
|
||||
if err == nil {
|
||||
// If parsing succeeded, validate the result
|
||||
|
||||
// The returned text should be a suffix of the input
|
||||
// (either the whole input if no frontmatter, or the part after frontmatter)
|
||||
if !strings.HasSuffix(input, text) && text != input {
|
||||
t.Errorf("returned text %q is not a valid suffix of input %q", text, input)
|
||||
}
|
||||
|
||||
// If input starts with delimiter and has valid frontmatter,
|
||||
// text should be shorter than input
|
||||
if strings.HasPrefix(input, "---\n") || strings.HasPrefix(input, "---\r\n") {
|
||||
if len(fm.Tags) > 0 || fm.Syntax != "" {
|
||||
// We successfully parsed frontmatter, so text should be shorter
|
||||
if len(text) >= len(input) {
|
||||
t.Errorf("text length %d should be less than input length %d when frontmatter is parsed",
|
||||
len(text), len(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Tags can be nil when frontmatter is not present or empty
|
||||
// This is expected behavior in Go for uninitialized slices
|
||||
} else {
|
||||
// If parsing failed, the original input should be returned as text
|
||||
if text != input {
|
||||
t.Errorf("on error, text should equal input: got %q, want %q", text, input)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseDelimiterHandling specifically tests delimiter edge cases
|
||||
func FuzzParseDelimiterHandling(f *testing.F) {
|
||||
// Seed corpus focusing on delimiter variations
|
||||
f.Add("---", "content")
|
||||
f.Add("", "---")
|
||||
f.Add("---", "---")
|
||||
f.Add("", "")
|
||||
|
||||
f.Fuzz(func(t *testing.T, prefix string, suffix string) {
|
||||
// Build input with controllable parts around delimiters
|
||||
inputs := []string{
|
||||
prefix + "---\n" + suffix,
|
||||
prefix + "---\r\n" + suffix,
|
||||
prefix + "---\n---\n" + suffix,
|
||||
prefix + "---\r\n---\r\n" + suffix,
|
||||
prefix + "---\n" + "yaml: data\n" + "---\n" + suffix,
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("parse panicked with constructed input: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
_, _, _ = parse(input)
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -9,17 +9,16 @@ import (
|
||||
func (s *Sheet) Search(reg *regexp.Regexp) string {
|
||||
|
||||
// record matches
|
||||
var matches []string
|
||||
matches := ""
|
||||
|
||||
// search through the cheatsheet's text line by line
|
||||
for _, line := range strings.Split(s.Text, "\n\n") {
|
||||
|
||||
// save matching lines
|
||||
// exit early if the line doesn't match the regex
|
||||
if reg.MatchString(line) {
|
||||
matches = append(matches, line)
|
||||
matches += line + "\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Join matches with the same delimiter used for splitting
|
||||
return strings.Join(matches, "\n\n")
|
||||
return strings.TrimSpace(matches)
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FuzzSearchRegex tests the regex compilation and search functionality
|
||||
// to ensure it handles malformed patterns gracefully and doesn't suffer
|
||||
// from catastrophic backtracking
|
||||
func FuzzSearchRegex(f *testing.F) {
|
||||
// Add seed corpus with various regex patterns
|
||||
// Valid patterns
|
||||
f.Add("test", "This is a test string")
|
||||
f.Add("(?i)test", "This is a TEST string")
|
||||
f.Add("foo|bar", "foo and bar")
|
||||
f.Add("^start", "start of line\nnext line")
|
||||
f.Add("end$", "at the end\nnext line")
|
||||
f.Add("\\d+", "123 numbers 456")
|
||||
f.Add("[a-z]+", "lowercase UPPERCASE")
|
||||
|
||||
// Edge cases and potentially problematic patterns
|
||||
f.Add("", "empty pattern")
|
||||
f.Add(".", "any character")
|
||||
f.Add(".*", "match everything")
|
||||
f.Add(".+", "match something")
|
||||
f.Add("\\", "backslash")
|
||||
f.Add("(", "unclosed paren")
|
||||
f.Add(")", "unmatched paren")
|
||||
f.Add("[", "unclosed bracket")
|
||||
f.Add("]", "unmatched bracket")
|
||||
f.Add("[^]", "negated empty class")
|
||||
f.Add("(?", "incomplete group")
|
||||
|
||||
// Patterns that might cause performance issues
|
||||
f.Add("(a+)+", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(a*)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(a|a)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(.*)*", "any text here")
|
||||
f.Add("(\\d+)+", "123456789012345678901234567890x")
|
||||
|
||||
// Unicode patterns
|
||||
f.Add("☺", "Unicode ☺ smiley")
|
||||
f.Add("[一-龯]", "Chinese 中文 characters")
|
||||
f.Add("\\p{L}+", "Unicode letters")
|
||||
|
||||
// Very long patterns
|
||||
f.Add(strings.Repeat("a", 1000), "long pattern")
|
||||
f.Add(strings.Repeat("(a|b)", 100), "complex pattern")
|
||||
|
||||
f.Fuzz(func(t *testing.T, pattern string, text string) {
|
||||
// Test 1: Regex compilation should not panic
|
||||
var reg *regexp.Regexp
|
||||
var compileErr error
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("regexp.Compile panicked with pattern %q: %v", pattern, r)
|
||||
}
|
||||
}()
|
||||
|
||||
reg, compileErr = regexp.Compile(pattern)
|
||||
}()
|
||||
|
||||
// If compilation failed, that's OK - we're testing error handling
|
||||
if compileErr != nil {
|
||||
// This is expected for invalid patterns
|
||||
return
|
||||
}
|
||||
|
||||
// Test 2: Create a sheet and test Search method
|
||||
sheet := Sheet{
|
||||
Title: "test",
|
||||
Text: text,
|
||||
}
|
||||
|
||||
// Search should not panic
|
||||
var result string
|
||||
done := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Search panicked with pattern %q on text %q: %v", pattern, text, r)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
result = sheet.Search(reg)
|
||||
}()
|
||||
|
||||
// Timeout after 100ms to catch catastrophic backtracking
|
||||
select {
|
||||
case <-done:
|
||||
// Search completed successfully
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Errorf("Search timed out (possible catastrophic backtracking) with pattern %q on text %q", pattern, text)
|
||||
}
|
||||
|
||||
// Test 3: Verify search result invariants
|
||||
if result != "" {
|
||||
// The Search function splits by "\n\n", so we need to compare using the same logic
|
||||
resultLines := strings.Split(result, "\n\n")
|
||||
textLines := strings.Split(text, "\n\n")
|
||||
|
||||
// Every result line should exist in the original text lines
|
||||
for _, rLine := range resultLines {
|
||||
found := false
|
||||
for _, tLine := range textLines {
|
||||
if rLine == tLine {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && rLine != "" {
|
||||
t.Errorf("Search result contains line not in original text: %q", rLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzTagged tests the Tagged function with potentially malicious tag inputs
|
||||
//
|
||||
// Threat model: An attacker crafts a malicious cheatsheet with specially
|
||||
// crafted tags that could cause issues when a user searches/filters by tags.
|
||||
// This is particularly relevant for shared community cheatsheets.
|
||||
func FuzzTagged(f *testing.F) {
|
||||
// Add seed corpus with potentially problematic inputs
|
||||
// These represent tags an attacker might use in a malicious cheatsheet
|
||||
f.Add("normal", "normal")
|
||||
f.Add("", "")
|
||||
f.Add(" ", " ")
|
||||
f.Add("\n", "\n")
|
||||
f.Add("\r\n", "\r\n")
|
||||
f.Add("\x00", "\x00") // Null byte
|
||||
f.Add("../../etc/passwd", "../../etc/passwd") // Path traversal attempt
|
||||
f.Add("'; DROP TABLE sheets;--", "sql") // SQL injection attempt
|
||||
f.Add("<script>alert('xss')</script>", "xss") // XSS attempt
|
||||
f.Add("${HOME}", "${HOME}") // Environment variable
|
||||
f.Add("$(whoami)", "$(whoami)") // Command substitution
|
||||
f.Add("`date`", "`date`") // Command substitution
|
||||
f.Add("\\x41\\x42", "\\x41\\x42") // Escape sequences
|
||||
f.Add("%00", "%00") // URL encoded null
|
||||
f.Add("tag\nwith\nnewlines", "tag")
|
||||
f.Add(strings.Repeat("a", 10000), "a") // Very long tag
|
||||
f.Add("🎉", "🎉") // Unicode
|
||||
f.Add("\U0001F4A9", "\U0001F4A9") // Unicode poop emoji
|
||||
f.Add("tag with spaces", "tag with spaces")
|
||||
f.Add("TAG", "tag") // Case sensitivity check
|
||||
f.Add("tag", "TAG") // Case sensitivity check
|
||||
|
||||
f.Fuzz(func(t *testing.T, sheetTag string, searchTag string) {
|
||||
// Create a sheet with the potentially malicious tag
|
||||
sheet := Sheet{
|
||||
Title: "test",
|
||||
Tags: []string{sheetTag},
|
||||
}
|
||||
|
||||
// The Tagged function should never panic regardless of input
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Tagged panicked with sheetTag=%q, searchTag=%q: %v",
|
||||
sheetTag, searchTag, r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := sheet.Tagged(searchTag)
|
||||
|
||||
// Verify the result is consistent with a simple string comparison
|
||||
expected := false
|
||||
for _, tag := range sheet.Tags {
|
||||
if tag == searchTag {
|
||||
expected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Tagged returned %v but expected %v for sheetTag=%q, searchTag=%q",
|
||||
result, expected, sheetTag, searchTag)
|
||||
}
|
||||
|
||||
// Additional invariant: Tagged should be case-sensitive
|
||||
if sheetTag != searchTag && result {
|
||||
t.Errorf("Tagged matched different strings: sheetTag=%q, searchTag=%q",
|
||||
sheetTag, searchTag)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test with multiple tags including the fuzzed one
|
||||
sheetMulti := Sheet{
|
||||
Title: "test",
|
||||
Tags: []string{"safe1", sheetTag, "safe2", sheetTag}, // Duplicate tags
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Tagged panicked with multiple tags including %q: %v",
|
||||
sheetTag, r)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = sheetMulti.Tagged(searchTag)
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
go test fuzz v1
|
||||
string("0")
|
||||
int(-6)
|
||||
int(5)
|
||||
@@ -1,3 +0,0 @@
|
||||
go test fuzz v1
|
||||
string(".")
|
||||
string(" 0000\n\n\n\n00000")
|
||||
@@ -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
|
||||
@@ -2,7 +2,6 @@ package sheets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
)
|
||||
@@ -32,8 +31,7 @@ func Filter(
|
||||
// iterate over each tag. If the sheet does not match *all* tags, filter
|
||||
// it out.
|
||||
for _, tag := range tags {
|
||||
trimmed := strings.TrimSpace(tag)
|
||||
if trimmed == "" || !utf8.ValidString(trimmed) || !sheet.Tagged(trimmed) {
|
||||
if !sheet.Tagged(strings.TrimSpace(tag)) {
|
||||
keep = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
)
|
||||
|
||||
// FuzzFilter tests the Filter function with various tag combinations
|
||||
func FuzzFilter(f *testing.F) {
|
||||
// Add seed corpus with various tag scenarios
|
||||
// Format: "tags to filter by" (comma-separated)
|
||||
f.Add("linux")
|
||||
f.Add("linux,bash")
|
||||
f.Add("linux,bash,ssh")
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(" linux ")
|
||||
f.Add("linux,")
|
||||
f.Add(",linux")
|
||||
f.Add(",,")
|
||||
f.Add("linux,,bash")
|
||||
f.Add("tag-with-dash")
|
||||
f.Add("tag_with_underscore")
|
||||
f.Add("UPPERCASE")
|
||||
f.Add("miXedCase")
|
||||
f.Add("🎉emoji")
|
||||
f.Add("tag with spaces")
|
||||
f.Add("\ttab\ttag")
|
||||
f.Add("tag\nwith\nnewline")
|
||||
f.Add("very-long-tag-name-that-might-cause-issues-somewhere")
|
||||
f.Add(strings.Repeat("a,", 100) + "a")
|
||||
|
||||
f.Fuzz(func(t *testing.T, tagString string) {
|
||||
// Split the tag string into individual tags
|
||||
var tags []string
|
||||
if tagString != "" {
|
||||
tags = strings.Split(tagString, ",")
|
||||
}
|
||||
|
||||
// Create test data - some sheets with various tags
|
||||
cheatpaths := []map[string]sheet.Sheet{
|
||||
{
|
||||
"sheet1": sheet.Sheet{
|
||||
Title: "sheet1",
|
||||
Tags: []string{"linux", "bash"},
|
||||
},
|
||||
"sheet2": sheet.Sheet{
|
||||
Title: "sheet2",
|
||||
Tags: []string{"linux", "ssh", "networking"},
|
||||
},
|
||||
"sheet3": sheet.Sheet{
|
||||
Title: "sheet3",
|
||||
Tags: []string{"UPPERCASE", "miXedCase"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"sheet4": sheet.Sheet{
|
||||
Title: "sheet4",
|
||||
Tags: []string{"tag with spaces", "🎉emoji"},
|
||||
},
|
||||
"sheet5": sheet.Sheet{
|
||||
Title: "sheet5",
|
||||
Tags: []string{}, // No tags
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// The function should not panic
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Filter panicked with tags %q: %v", tags, r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := Filter(cheatpaths, tags)
|
||||
|
||||
// Verify invariants
|
||||
// 1. Result should have same number of cheatpaths
|
||||
if len(result) != len(cheatpaths) {
|
||||
t.Errorf("Filter changed number of cheatpaths: got %d, want %d",
|
||||
len(result), len(cheatpaths))
|
||||
}
|
||||
|
||||
// 2. Each filtered sheet should contain all requested tags
|
||||
for _, filteredPath := range result {
|
||||
for title, sheet := range filteredPath {
|
||||
// Verify this sheet has all the tags we filtered for
|
||||
for _, tag := range tags {
|
||||
trimmedTag := strings.TrimSpace(tag)
|
||||
if trimmedTag == "" {
|
||||
continue // Skip empty tags
|
||||
}
|
||||
if !sheet.Tagged(trimmedTag) {
|
||||
t.Errorf("Sheet %q passed filter but doesn't have tag %q",
|
||||
title, trimmedTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Empty tag list should return all sheets
|
||||
if len(tags) == 0 || (len(tags) == 1 && tags[0] == "") {
|
||||
totalOriginal := 0
|
||||
totalFiltered := 0
|
||||
for _, path := range cheatpaths {
|
||||
totalOriginal += len(path)
|
||||
}
|
||||
for _, path := range result {
|
||||
totalFiltered += len(path)
|
||||
}
|
||||
if totalFiltered != totalOriginal {
|
||||
t.Errorf("Empty filter should return all sheets: got %d, want %d",
|
||||
totalFiltered, totalOriginal)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzFilterEdgeCases tests Filter with extreme inputs
|
||||
func FuzzFilterEdgeCases(f *testing.F) {
|
||||
// Seed with number of tags and tag length
|
||||
f.Add(0, 0)
|
||||
f.Add(1, 10)
|
||||
f.Add(10, 10)
|
||||
f.Add(100, 5)
|
||||
f.Add(1000, 3)
|
||||
|
||||
f.Fuzz(func(t *testing.T, numTags int, tagLen int) {
|
||||
// Limit to reasonable values to avoid memory issues
|
||||
if numTags > 1000 || numTags < 0 || tagLen > 100 || tagLen < 0 {
|
||||
t.Skip("Skipping unreasonable test case")
|
||||
}
|
||||
|
||||
// Generate tags
|
||||
tags := make([]string, numTags)
|
||||
for i := 0; i < numTags; i++ {
|
||||
// Create a tag of specified length
|
||||
if tagLen > 0 {
|
||||
tags[i] = strings.Repeat("a", tagLen) + string(rune(i%26+'a'))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a sheet with no tags (should be filtered out)
|
||||
cheatpaths := []map[string]sheet.Sheet{
|
||||
{
|
||||
"test": sheet.Sheet{
|
||||
Title: "test",
|
||||
Tags: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Should not panic with many tags
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Filter panicked with %d tags of length %d: %v",
|
||||
numTags, tagLen, r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := Filter(cheatpaths, tags)
|
||||
|
||||
// With non-matching tags, result should be empty
|
||||
if numTags > 0 && tagLen > 0 {
|
||||
if len(result[0]) != 0 {
|
||||
t.Errorf("Expected empty result with non-matching tags, got %d sheets",
|
||||
len(result[0]))
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
||||
sheets := make([]map[string]sheet.Sheet, len(cheatpaths))
|
||||
|
||||
// iterate over each cheatpath
|
||||
for i, cheatpath := range cheatpaths {
|
||||
for _, cheatpath := range cheatpaths {
|
||||
|
||||
// vivify the map of cheatsheets on this specific cheatpath
|
||||
pathsheets := make(map[string]sheet.Sheet)
|
||||
@@ -43,19 +43,6 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the base filename
|
||||
filename := filepath.Base(path)
|
||||
|
||||
// skip hidden files (files that start with a dot)
|
||||
if strings.HasPrefix(filename, ".") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// skip files with extensions (cheatsheets have no extension)
|
||||
if filepath.Ext(filename) != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculate the cheatsheet's "title" (the phrase with which it may be
|
||||
// accessed. Eg: `cheat tar` - `tar` is the title)
|
||||
title := strings.TrimPrefix(
|
||||
@@ -101,7 +88,7 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
||||
|
||||
// store the sheets on this cheatpath alongside the other cheatsheets on
|
||||
// other cheatpaths
|
||||
sheets[i] = pathsheets
|
||||
sheets = append(sheets, pathsheets)
|
||||
}
|
||||
|
||||
// return the cheatsheets, grouped by cheatpath
|
||||
|
||||
@@ -26,26 +26,19 @@ func TestLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
// load cheatsheets
|
||||
cheatpathSheets, err := Load(cheatpaths)
|
||||
sheets, err := Load(cheatpaths)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load cheatsheets: %v", err)
|
||||
}
|
||||
|
||||
// assert that the correct number of sheets loaded
|
||||
// (sheet load details are tested in `sheet_test.go`)
|
||||
totalSheets := 0
|
||||
for _, sheets := range cheatpathSheets {
|
||||
totalSheets += len(sheets)
|
||||
}
|
||||
|
||||
// we expect 4 total sheets (2 from community, 2 from personal)
|
||||
// hidden files and files with extensions are excluded
|
||||
want := 4
|
||||
if totalSheets != want {
|
||||
if len(sheets) != want {
|
||||
t.Errorf(
|
||||
"failed to load correct number of cheatsheets: want: %d, got: %d",
|
||||
want,
|
||||
totalSheets,
|
||||
len(sheets),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package sheets
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
)
|
||||
@@ -17,10 +16,7 @@ func Tags(cheatpaths []map[string]sheet.Sheet) []string {
|
||||
for _, path := range cheatpaths {
|
||||
for _, sheet := range path {
|
||||
for _, tag := range sheet.Tags {
|
||||
// Skip invalid UTF-8 tags to prevent downstream issues
|
||||
if utf8.ValidString(tag) {
|
||||
tags[tag] = true
|
||||
}
|
||||
tags[tag] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
)
|
||||
|
||||
// FuzzTags tests the Tags function with various tag combinations
|
||||
func FuzzTags(f *testing.F) {
|
||||
// Add seed corpus
|
||||
// Format: comma-separated tags that will be distributed across sheets
|
||||
f.Add("linux,bash,ssh")
|
||||
f.Add("")
|
||||
f.Add("single")
|
||||
f.Add("duplicate,duplicate,duplicate")
|
||||
f.Add(" spaces , around , tags ")
|
||||
f.Add("MiXeD,UPPER,lower")
|
||||
f.Add("special-chars,under_score,dot.ted")
|
||||
f.Add("emoji🎉,unicode中文,symbols@#$")
|
||||
f.Add("\ttab,\nnewline,\rcarriage")
|
||||
f.Add(",,,,") // Multiple empty tags
|
||||
f.Add(strings.Repeat("tag,", 100)) // Many tags
|
||||
f.Add("a," + strings.Repeat("very-long-tag-name", 10)) // Long tag names
|
||||
|
||||
f.Fuzz(func(t *testing.T, tagString string) {
|
||||
// Split tags and distribute them across multiple sheets
|
||||
var allTags []string
|
||||
if tagString != "" {
|
||||
allTags = strings.Split(tagString, ",")
|
||||
}
|
||||
|
||||
// Create test cheatpaths with various tag distributions
|
||||
cheatpaths := []map[string]sheet.Sheet{}
|
||||
|
||||
// Distribute tags across 3 paths with overlapping tags
|
||||
for i := 0; i < 3; i++ {
|
||||
path := make(map[string]sheet.Sheet)
|
||||
|
||||
// Each path gets some subset of tags
|
||||
for j, tag := range allTags {
|
||||
if j%3 == i || j%(i+2) == 0 { // Create some overlap
|
||||
sheetName := string(rune('a' + j%26))
|
||||
path[sheetName] = sheet.Sheet{
|
||||
Title: sheetName,
|
||||
Tags: []string{tag},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a sheet with multiple tags
|
||||
if len(allTags) > 1 {
|
||||
path["multi"] = sheet.Sheet{
|
||||
Title: "multi",
|
||||
Tags: allTags[:len(allTags)/2+1], // First half of tags
|
||||
}
|
||||
}
|
||||
|
||||
cheatpaths = append(cheatpaths, path)
|
||||
}
|
||||
|
||||
// The function should not panic
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Tags panicked with input %q: %v", tagString, r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := Tags(cheatpaths)
|
||||
|
||||
// Verify invariants
|
||||
// 1. Result should be sorted
|
||||
for i := 1; i < len(result); i++ {
|
||||
if result[i-1] >= result[i] {
|
||||
t.Errorf("Tags not sorted: %q >= %q at positions %d, %d",
|
||||
result[i-1], result[i], i-1, i)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. No duplicates in result
|
||||
seen := make(map[string]bool)
|
||||
for _, tag := range result {
|
||||
if seen[tag] {
|
||||
t.Errorf("Duplicate tag in result: %q", tag)
|
||||
}
|
||||
seen[tag] = true
|
||||
}
|
||||
|
||||
// 3. All non-empty tags from input should be in result
|
||||
// (This is approximate since we distributed tags in a complex way)
|
||||
inputTags := make(map[string]bool)
|
||||
for _, tag := range allTags {
|
||||
if tag != "" {
|
||||
inputTags[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
resultTags := make(map[string]bool)
|
||||
for _, tag := range result {
|
||||
resultTags[tag] = true
|
||||
}
|
||||
|
||||
// Result might have fewer tags due to distribution logic,
|
||||
// but shouldn't have tags not in the input
|
||||
for tag := range resultTags {
|
||||
found := false
|
||||
for inputTag := range inputTags {
|
||||
if tag == inputTag {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && tag != "" {
|
||||
t.Errorf("Result contains tag %q not derived from input", tag)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Valid UTF-8 (Tags function should filter out invalid UTF-8)
|
||||
for _, tag := range result {
|
||||
if !utf8.ValidString(tag) {
|
||||
t.Errorf("Invalid UTF-8 in tag: %q", tag)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
go test fuzz v1
|
||||
string("\xd7")
|
||||
@@ -1,2 +0,0 @@
|
||||
go test fuzz v1
|
||||
string("\xf0")
|
||||
16
vendor/github.com/go-git/go-git/v5/COMPATIBILITY.md
generated
vendored
16
vendor/github.com/go-git/go-git/v5/COMPATIBILITY.md
generated
vendored
@@ -27,14 +27,14 @@ compatibility status with go-git.
|
||||
|
||||
## Branching and merging
|
||||
|
||||
| Feature | Sub-feature | Status | Notes | Examples |
|
||||
| ----------- | ----------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
|
||||
| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
|
||||
| `merge` | | ❌ | | |
|
||||
| `mergetool` | | ❌ | | |
|
||||
| `stash` | | ❌ | | |
|
||||
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
|
||||
| Feature | Sub-feature | Status | Notes | Examples |
|
||||
| ----------- | ----------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `branch` | | ✅ | | - [branch](_examples/branch/main.go) |
|
||||
| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) |
|
||||
| `merge` | | ⚠️ (partial) | Fast-forward only | |
|
||||
| `mergetool` | | ❌ | | |
|
||||
| `stash` | | ❌ | | |
|
||||
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
|
||||
|
||||
## Sharing and updating projects
|
||||
|
||||
|
||||
1
vendor/github.com/go-git/go-git/v5/Makefile
generated
vendored
1
vendor/github.com/go-git/go-git/v5/Makefile
generated
vendored
@@ -28,6 +28,7 @@ build-git:
|
||||
test:
|
||||
@echo "running against `git version`"; \
|
||||
$(GOTEST) -race ./...
|
||||
$(GOTEST) -v _examples/common_test.go _examples/common.go --examples
|
||||
|
||||
TEMP_REPO := $(shell mktemp)
|
||||
test-sha256:
|
||||
|
||||
44
vendor/github.com/go-git/go-git/v5/options.go
generated
vendored
44
vendor/github.com/go-git/go-git/v5/options.go
generated
vendored
@@ -89,6 +89,25 @@ type CloneOptions struct {
|
||||
Shared bool
|
||||
}
|
||||
|
||||
// MergeOptions describes how a merge should be performed.
|
||||
type MergeOptions struct {
|
||||
// Strategy defines the merge strategy to be used.
|
||||
Strategy MergeStrategy
|
||||
}
|
||||
|
||||
// MergeStrategy represents the different types of merge strategies.
|
||||
type MergeStrategy int8
|
||||
|
||||
const (
|
||||
// FastForwardMerge represents a Git merge strategy where the current
|
||||
// branch can be simply updated to point to the HEAD of the branch being
|
||||
// merged. This is only possible if the history of the branch being merged
|
||||
// is a linear descendant of the current branch, with no conflicting commits.
|
||||
//
|
||||
// This is the default option.
|
||||
FastForwardMerge MergeStrategy = iota
|
||||
)
|
||||
|
||||
// Validate validates the fields and sets the default values.
|
||||
func (o *CloneOptions) Validate() error {
|
||||
if o.URL == "" {
|
||||
@@ -166,7 +185,7 @@ const (
|
||||
// AllTags fetch all tags from the remote (i.e., fetch remote tags
|
||||
// refs/tags/* into local tags with the same name)
|
||||
AllTags
|
||||
//NoTags fetch no tags from the remote at all
|
||||
// NoTags fetch no tags from the remote at all
|
||||
NoTags
|
||||
)
|
||||
|
||||
@@ -198,6 +217,9 @@ type FetchOptions struct {
|
||||
CABundle []byte
|
||||
// ProxyOptions provides info required for connecting to a proxy.
|
||||
ProxyOptions transport.ProxyOptions
|
||||
// Prune specify that local refs that match given RefSpecs and that do
|
||||
// not exist remotely will be removed.
|
||||
Prune bool
|
||||
}
|
||||
|
||||
// Validate validates the fields and sets the default values.
|
||||
@@ -324,9 +346,9 @@ var (
|
||||
|
||||
// CheckoutOptions describes how a checkout operation should be performed.
|
||||
type CheckoutOptions struct {
|
||||
// Hash is the hash of the commit to be checked out. If used, HEAD will be
|
||||
// in detached mode. If Create is not used, Branch and Hash are mutually
|
||||
// exclusive.
|
||||
// Hash is the hash of a commit or tag to be checked out. If used, HEAD
|
||||
// will be in detached mode. If Create is not used, Branch and Hash are
|
||||
// mutually exclusive.
|
||||
Hash plumbing.Hash
|
||||
// Branch to be checked out, if Branch and Hash are empty is set to `master`.
|
||||
Branch plumbing.ReferenceName
|
||||
@@ -405,6 +427,11 @@ func (o *ResetOptions) Validate(r *Repository) error {
|
||||
}
|
||||
|
||||
o.Commit = ref.Hash()
|
||||
} else {
|
||||
_, err := r.CommitObject(o.Commit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid reset option: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -474,6 +501,11 @@ type AddOptions struct {
|
||||
// Glob adds all paths, matching pattern, to the index. If pattern matches a
|
||||
// directory path, all directory contents are added to the index recursively.
|
||||
Glob string
|
||||
// SkipStatus adds the path with no status check. This option is relevant only
|
||||
// when the `Path` option is specified and does not apply when the `All` option is used.
|
||||
// Notice that when passing an ignored path it will be added anyway.
|
||||
// When true it can speed up adding files to the worktree in very large repositories.
|
||||
SkipStatus bool
|
||||
}
|
||||
|
||||
// Validate validates the fields and sets the default values.
|
||||
@@ -507,6 +539,10 @@ type CommitOptions struct {
|
||||
// commit will not be signed. The private key must be present and already
|
||||
// decrypted.
|
||||
SignKey *openpgp.Entity
|
||||
// Signer denotes a cryptographic signer to sign the commit with.
|
||||
// A nil value here means the commit will not be signed.
|
||||
// Takes precedence over SignKey.
|
||||
Signer Signer
|
||||
// Amend will create a new commit object and replace the commit that HEAD currently
|
||||
// points to. Cannot be used with All nor Parents.
|
||||
Amend bool
|
||||
|
||||
4
vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/dir.go
generated
vendored
4
vendor/github.com/go-git/go-git/v5/plumbing/format/gitignore/dir.go
generated
vendored
@@ -116,7 +116,7 @@ func loadPatterns(fs billy.Filesystem, path string) (ps []Pattern, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// LoadGlobalPatterns loads gitignore patterns from from the gitignore file
|
||||
// LoadGlobalPatterns loads gitignore patterns from the gitignore file
|
||||
// declared in a user's ~/.gitconfig file. If the ~/.gitconfig file does not
|
||||
// exist the function will return nil. If the core.excludesfile property
|
||||
// is not declared, the function will return nil. If the file pointed to by
|
||||
@@ -132,7 +132,7 @@ func LoadGlobalPatterns(fs billy.Filesystem) (ps []Pattern, err error) {
|
||||
return loadPatterns(fs, fs.Join(home, gitconfigFile))
|
||||
}
|
||||
|
||||
// LoadSystemPatterns loads gitignore patterns from from the gitignore file
|
||||
// LoadSystemPatterns loads gitignore patterns from the gitignore file
|
||||
// declared in a system's /etc/gitconfig file. If the /etc/gitconfig file does
|
||||
// not exist the function will return nil. If the core.excludesfile property
|
||||
// is not declared, the function will return nil. If the file pointed to by
|
||||
|
||||
6
vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go
generated
vendored
6
vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go
generated
vendored
@@ -27,7 +27,7 @@ const (
|
||||
// the commit with the "mergetag" header.
|
||||
headermergetag string = "mergetag"
|
||||
|
||||
defaultUtf8CommitMesageEncoding MessageEncoding = "UTF-8"
|
||||
defaultUtf8CommitMessageEncoding MessageEncoding = "UTF-8"
|
||||
)
|
||||
|
||||
// Hash represents the hash of an object
|
||||
@@ -189,7 +189,7 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
}
|
||||
|
||||
c.Hash = o.Hash()
|
||||
c.Encoding = defaultUtf8CommitMesageEncoding
|
||||
c.Encoding = defaultUtf8CommitMessageEncoding
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@@ -335,7 +335,7 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMesageEncoding {
|
||||
if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMessageEncoding {
|
||||
if _, err = fmt.Fprintf(w, "\n%s %s", headerencoding, c.Encoding); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
19
vendor/github.com/go-git/go-git/v5/plumbing/object/commit_walker_path.go
generated
vendored
19
vendor/github.com/go-git/go-git/v5/plumbing/object/commit_walker_path.go
generated
vendored
@@ -57,6 +57,8 @@ func (c *commitPathIter) Next() (*Commit, error) {
|
||||
}
|
||||
|
||||
func (c *commitPathIter) getNextFileCommit() (*Commit, error) {
|
||||
var parentTree, currentTree *Tree
|
||||
|
||||
for {
|
||||
// Parent-commit can be nil if the current-commit is the initial commit
|
||||
parentCommit, parentCommitErr := c.sourceIter.Next()
|
||||
@@ -68,13 +70,17 @@ func (c *commitPathIter) getNextFileCommit() (*Commit, error) {
|
||||
parentCommit = nil
|
||||
}
|
||||
|
||||
// Fetch the trees of the current and parent commits
|
||||
currentTree, currTreeErr := c.currentCommit.Tree()
|
||||
if currTreeErr != nil {
|
||||
return nil, currTreeErr
|
||||
if parentTree == nil {
|
||||
var currTreeErr error
|
||||
currentTree, currTreeErr = c.currentCommit.Tree()
|
||||
if currTreeErr != nil {
|
||||
return nil, currTreeErr
|
||||
}
|
||||
} else {
|
||||
currentTree = parentTree
|
||||
parentTree = nil
|
||||
}
|
||||
|
||||
var parentTree *Tree
|
||||
if parentCommit != nil {
|
||||
var parentTreeErr error
|
||||
parentTree, parentTreeErr = parentCommit.Tree()
|
||||
@@ -115,7 +121,8 @@ func (c *commitPathIter) hasFileChange(changes Changes, parent *Commit) bool {
|
||||
|
||||
// filename matches, now check if source iterator contains all commits (from all refs)
|
||||
if c.checkParent {
|
||||
if parent != nil && isParentHash(parent.Hash, c.currentCommit) {
|
||||
// Check if parent is beyond the initial commit
|
||||
if parent == nil || isParentHash(parent.Hash, c.currentCommit) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
|
||||
97
vendor/github.com/go-git/go-git/v5/plumbing/object/patch.go
generated
vendored
97
vendor/github.com/go-git/go-git/v5/plumbing/object/patch.go
generated
vendored
@@ -6,7 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
@@ -234,69 +234,56 @@ func (fileStats FileStats) String() string {
|
||||
return printStat(fileStats)
|
||||
}
|
||||
|
||||
// printStat prints the stats of changes in content of files.
|
||||
// Original implementation: https://github.com/git/git/blob/1a87c842ece327d03d08096395969aca5e0a6996/diff.c#L2615
|
||||
// Parts of the output:
|
||||
// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
|
||||
// example: " main.go | 10 +++++++--- "
|
||||
func printStat(fileStats []FileStat) string {
|
||||
padLength := float64(len(" "))
|
||||
newlineLength := float64(len("\n"))
|
||||
separatorLength := float64(len("|"))
|
||||
// Soft line length limit. The text length calculation below excludes
|
||||
// length of the change number. Adding that would take it closer to 80,
|
||||
// but probably not more than 80, until it's a huge number.
|
||||
lineLength := 72.0
|
||||
maxGraphWidth := uint(53)
|
||||
maxNameLen := 0
|
||||
maxChangeLen := 0
|
||||
|
||||
scaleLinear := func(it, width, max uint) uint {
|
||||
if it == 0 || max == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1 + (it * (width - 1) / max)
|
||||
}
|
||||
|
||||
// Get the longest filename and longest total change.
|
||||
var longestLength float64
|
||||
var longestTotalChange float64
|
||||
for _, fs := range fileStats {
|
||||
if int(longestLength) < len(fs.Name) {
|
||||
longestLength = float64(len(fs.Name))
|
||||
if len(fs.Name) > maxNameLen {
|
||||
maxNameLen = len(fs.Name)
|
||||
}
|
||||
totalChange := fs.Addition + fs.Deletion
|
||||
if int(longestTotalChange) < totalChange {
|
||||
longestTotalChange = float64(totalChange)
|
||||
|
||||
changes := strconv.Itoa(fs.Addition + fs.Deletion)
|
||||
if len(changes) > maxChangeLen {
|
||||
maxChangeLen = len(changes)
|
||||
}
|
||||
}
|
||||
|
||||
// Parts of the output:
|
||||
// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
|
||||
// example: " main.go | 10 +++++++--- "
|
||||
|
||||
// <pad><filename><pad>
|
||||
leftTextLength := padLength + longestLength + padLength
|
||||
|
||||
// <pad><number><pad><+++++/-----><newline>
|
||||
// Excluding number length here.
|
||||
rightTextLength := padLength + padLength + newlineLength
|
||||
|
||||
totalTextArea := leftTextLength + separatorLength + rightTextLength
|
||||
heightOfHistogram := lineLength - totalTextArea
|
||||
|
||||
// Scale the histogram.
|
||||
var scaleFactor float64
|
||||
if longestTotalChange > heightOfHistogram {
|
||||
// Scale down to heightOfHistogram.
|
||||
scaleFactor = longestTotalChange / heightOfHistogram
|
||||
} else {
|
||||
scaleFactor = 1.0
|
||||
}
|
||||
|
||||
finalOutput := ""
|
||||
result := ""
|
||||
for _, fs := range fileStats {
|
||||
addn := float64(fs.Addition)
|
||||
deln := float64(fs.Deletion)
|
||||
addc := int(math.Floor(addn/scaleFactor))
|
||||
delc := int(math.Floor(deln/scaleFactor))
|
||||
if addc < 0 {
|
||||
addc = 0
|
||||
}
|
||||
if delc < 0 {
|
||||
delc = 0
|
||||
}
|
||||
adds := strings.Repeat("+", addc)
|
||||
dels := strings.Repeat("-", delc)
|
||||
finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels)
|
||||
}
|
||||
add := uint(fs.Addition)
|
||||
del := uint(fs.Deletion)
|
||||
np := maxNameLen - len(fs.Name)
|
||||
cp := maxChangeLen - len(strconv.Itoa(fs.Addition+fs.Deletion))
|
||||
|
||||
return finalOutput
|
||||
total := add + del
|
||||
if total > maxGraphWidth {
|
||||
add = scaleLinear(add, maxGraphWidth, total)
|
||||
del = scaleLinear(del, maxGraphWidth, total)
|
||||
}
|
||||
|
||||
adds := strings.Repeat("+", int(add))
|
||||
dels := strings.Repeat("-", int(del))
|
||||
namePad := strings.Repeat(" ", np)
|
||||
changePad := strings.Repeat(" ", cp)
|
||||
|
||||
result += fmt.Sprintf(" %s%s | %s%d %s%s\n", fs.Name, namePad, changePad, total, adds, dels)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats {
|
||||
|
||||
32
vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go
generated
vendored
32
vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go
generated
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
@@ -27,6 +28,7 @@ var (
|
||||
ErrFileNotFound = errors.New("file not found")
|
||||
ErrDirectoryNotFound = errors.New("directory not found")
|
||||
ErrEntryNotFound = errors.New("entry not found")
|
||||
ErrEntriesNotSorted = errors.New("entries in tree are not sorted")
|
||||
)
|
||||
|
||||
// Tree is basically like a directory - it references a bunch of other trees
|
||||
@@ -270,6 +272,28 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
type TreeEntrySorter []TreeEntry
|
||||
|
||||
func (s TreeEntrySorter) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Less(i, j int) bool {
|
||||
name1 := s[i].Name
|
||||
name2 := s[j].Name
|
||||
if s[i].Mode == filemode.Dir {
|
||||
name1 += "/"
|
||||
}
|
||||
if s[j].Mode == filemode.Dir {
|
||||
name2 += "/"
|
||||
}
|
||||
return name1 < name2
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Encode transforms a Tree into a plumbing.EncodedObject.
|
||||
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
o.SetType(plumbing.TreeObject)
|
||||
@@ -279,7 +303,15 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
}
|
||||
|
||||
defer ioutil.CheckClose(w, &err)
|
||||
|
||||
if !sort.IsSorted(TreeEntrySorter(t.Entries)) {
|
||||
return ErrEntriesNotSorted
|
||||
}
|
||||
|
||||
for _, entry := range t.Entries {
|
||||
if strings.IndexByte(entry.Name, 0) != -1 {
|
||||
return fmt.Errorf("malformed filename %q", entry.Name)
|
||||
}
|
||||
if _, err = fmt.Fprintf(w, "%o %s", entry.Mode, entry.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
4
vendor/github.com/go-git/go-git/v5/plumbing/object/treenoder.go
generated
vendored
4
vendor/github.com/go-git/go-git/v5/plumbing/object/treenoder.go
generated
vendored
@@ -88,7 +88,9 @@ func (t *treeNoder) Children() ([]noder.Noder, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return transformChildren(parent)
|
||||
var err error
|
||||
t.children, err = transformChildren(parent)
|
||||
return t.children, err
|
||||
}
|
||||
|
||||
// Returns the children of a tree as treenoders.
|
||||
|
||||
20
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go
generated
vendored
20
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go
generated
vendored
@@ -91,9 +91,9 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
|
||||
}
|
||||
|
||||
type client struct {
|
||||
c *http.Client
|
||||
client *http.Client
|
||||
transports *lru.Cache
|
||||
m sync.RWMutex
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ClientOptions holds user configurable options for the client.
|
||||
@@ -147,7 +147,7 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo
|
||||
}
|
||||
}
|
||||
cl := &client{
|
||||
c: c,
|
||||
client: c,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
@@ -234,10 +234,10 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*
|
||||
// if the client wasn't configured to have a cache for transports then just configure
|
||||
// the transport and use it directly, otherwise try to use the cache.
|
||||
if c.transports == nil {
|
||||
tr, ok := c.c.Transport.(*http.Transport)
|
||||
tr, ok := c.client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected underlying client transport to be of type: %s; got: %s",
|
||||
reflect.TypeOf(transport), reflect.TypeOf(c.c.Transport))
|
||||
reflect.TypeOf(transport), reflect.TypeOf(c.client.Transport))
|
||||
}
|
||||
|
||||
transport = tr.Clone()
|
||||
@@ -258,7 +258,7 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*
|
||||
transport, found = c.fetchTransport(transportOpts)
|
||||
|
||||
if !found {
|
||||
transport = c.c.Transport.(*http.Transport).Clone()
|
||||
transport = c.client.Transport.(*http.Transport).Clone()
|
||||
configureTransport(transport, ep)
|
||||
c.addTransport(transportOpts, transport)
|
||||
}
|
||||
@@ -266,12 +266,12 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: c.c.CheckRedirect,
|
||||
Jar: c.c.Jar,
|
||||
Timeout: c.c.Timeout,
|
||||
CheckRedirect: c.client.CheckRedirect,
|
||||
Jar: c.client.Jar,
|
||||
Timeout: c.client.Timeout,
|
||||
}
|
||||
} else {
|
||||
httpClient = c.c
|
||||
httpClient = c.client
|
||||
}
|
||||
|
||||
s := &session{
|
||||
|
||||
12
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go
generated
vendored
12
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/transport.go
generated
vendored
@@ -14,21 +14,21 @@ type transportOptions struct {
|
||||
}
|
||||
|
||||
func (c *client) addTransport(opts transportOptions, transport *http.Transport) {
|
||||
c.m.Lock()
|
||||
c.mutex.Lock()
|
||||
c.transports.Add(opts, transport)
|
||||
c.m.Unlock()
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *client) removeTransport(opts transportOptions) {
|
||||
c.m.Lock()
|
||||
c.mutex.Lock()
|
||||
c.transports.Remove(opts)
|
||||
c.m.Unlock()
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *client) fetchTransport(opts transportOptions) (*http.Transport, bool) {
|
||||
c.m.RLock()
|
||||
c.mutex.RLock()
|
||||
t, ok := c.transports.Get(opts)
|
||||
c.m.RUnlock()
|
||||
c.mutex.RUnlock()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
4
vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go
generated
vendored
4
vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go
generated
vendored
@@ -49,7 +49,9 @@ type runner struct {
|
||||
func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (common.Command, error) {
|
||||
c := &command{command: cmd, endpoint: ep, config: r.config}
|
||||
if auth != nil {
|
||||
c.setAuth(auth)
|
||||
if err := c.setAuth(auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.connect(); err != nil {
|
||||
|
||||
33
vendor/github.com/go-git/go-git/v5/remote.go
generated
vendored
33
vendor/github.com/go-git/go-git/v5/remote.go
generated
vendored
@@ -470,6 +470,14 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
|
||||
}
|
||||
}
|
||||
|
||||
var updatedPrune bool
|
||||
if o.Prune {
|
||||
updatedPrune, err = r.pruneRemotes(o.RefSpecs, localRefs, remoteRefs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := r.updateLocalReferenceStorage(o.RefSpecs, refs, remoteRefs, specToRefs, o.Tags, o.Force)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -482,7 +490,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
|
||||
}
|
||||
}
|
||||
|
||||
if !updated {
|
||||
if !updated && !updatedPrune {
|
||||
return remoteRefs, NoErrAlreadyUpToDate
|
||||
}
|
||||
|
||||
@@ -574,6 +582,27 @@ func (r *Remote) fetchPack(ctx context.Context, o *FetchOptions, s transport.Upl
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Remote) pruneRemotes(specs []config.RefSpec, localRefs []*plumbing.Reference, remoteRefs memory.ReferenceStorage) (bool, error) {
|
||||
var updatedPrune bool
|
||||
for _, spec := range specs {
|
||||
rev := spec.Reverse()
|
||||
for _, ref := range localRefs {
|
||||
if !rev.Match(ref.Name()) {
|
||||
continue
|
||||
}
|
||||
_, err := remoteRefs.Reference(rev.Dst(ref.Name()))
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
updatedPrune = true
|
||||
err := r.s.RemoveReference(ref.Name())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updatedPrune, nil
|
||||
}
|
||||
|
||||
func (r *Remote) addReferencesToUpdate(
|
||||
refspecs []config.RefSpec,
|
||||
localRefs []*plumbing.Reference,
|
||||
@@ -1099,7 +1128,7 @@ func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash, earlies
|
||||
}
|
||||
|
||||
found := false
|
||||
// stop iterating at the earlist shallow commit, ignoring its parents
|
||||
// stop iterating at the earliest shallow commit, ignoring its parents
|
||||
// note: when pull depth is smaller than the number of new changes on the remote, this fails due to missing parents.
|
||||
// as far as i can tell, without the commits in-between the shallow pull and the earliest shallow, there's no
|
||||
// real way of telling whether it will be a fast-forward merge.
|
||||
|
||||
65
vendor/github.com/go-git/go-git/v5/repository.go
generated
vendored
65
vendor/github.com/go-git/go-git/v5/repository.go
generated
vendored
@@ -51,19 +51,21 @@ var (
|
||||
// ErrFetching is returned when the packfile could not be downloaded
|
||||
ErrFetching = errors.New("unable to fetch packfile")
|
||||
|
||||
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
|
||||
ErrRepositoryNotExists = errors.New("repository does not exist")
|
||||
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
|
||||
ErrRepositoryAlreadyExists = errors.New("repository already exists")
|
||||
ErrRemoteNotFound = errors.New("remote not found")
|
||||
ErrRemoteExists = errors.New("remote already exists")
|
||||
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
|
||||
ErrWorktreeNotProvided = errors.New("worktree should be provided")
|
||||
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
|
||||
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
|
||||
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
|
||||
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
|
||||
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
|
||||
ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch")
|
||||
ErrRepositoryNotExists = errors.New("repository does not exist")
|
||||
ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist")
|
||||
ErrRepositoryAlreadyExists = errors.New("repository already exists")
|
||||
ErrRemoteNotFound = errors.New("remote not found")
|
||||
ErrRemoteExists = errors.New("remote already exists")
|
||||
ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'")
|
||||
ErrWorktreeNotProvided = errors.New("worktree should be provided")
|
||||
ErrIsBareRepository = errors.New("worktree not available in a bare repository")
|
||||
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
|
||||
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
|
||||
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
|
||||
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
|
||||
ErrUnsupportedMergeStrategy = errors.New("unsupported merge strategy")
|
||||
ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes")
|
||||
)
|
||||
|
||||
// Repository represents a git repository
|
||||
@@ -1769,8 +1771,43 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge merges the reference branch into the current branch.
|
||||
//
|
||||
// If the merge is not possible (or supported) returns an error without changing
|
||||
// the HEAD for the current branch. Possible errors include:
|
||||
// - The merge strategy is not supported.
|
||||
// - The specific strategy cannot be used (e.g. using FastForwardMerge when one is not possible).
|
||||
func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error {
|
||||
if opts.Strategy != FastForwardMerge {
|
||||
return ErrUnsupportedMergeStrategy
|
||||
}
|
||||
|
||||
// Ignore error as not having a shallow list is optional here.
|
||||
shallowList, _ := r.Storer.Shallow()
|
||||
var earliestShallow *plumbing.Hash
|
||||
if len(shallowList) > 0 {
|
||||
earliestShallow = &shallowList[0]
|
||||
}
|
||||
|
||||
head, err := r.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ff {
|
||||
return ErrFastForwardMergeNotPossible
|
||||
}
|
||||
|
||||
return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash()))
|
||||
}
|
||||
|
||||
// createNewObjectPack is a helper for RepackObjects taking care
|
||||
// of creating a new pack. It is used so the the PackfileWriter
|
||||
// of creating a new pack. It is used so the PackfileWriter
|
||||
// deferred close has the right scope.
|
||||
func (r *Repository) createNewObjectPack(cfg *RepackConfig) (h plumbing.Hash, err error) {
|
||||
ow := newObjectWalker(r.Storer)
|
||||
|
||||
33
vendor/github.com/go-git/go-git/v5/signer.go
generated
vendored
Normal file
33
vendor/github.com/go-git/go-git/v5/signer.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// signableObject is an object which can be signed.
|
||||
type signableObject interface {
|
||||
EncodeWithoutSignature(o plumbing.EncodedObject) error
|
||||
}
|
||||
|
||||
// Signer is an interface for signing git objects.
|
||||
// message is a reader containing the encoded object to be signed.
|
||||
// Implementors should return the encoded signature and an error if any.
|
||||
// See https://git-scm.com/docs/gitformat-signature for more information.
|
||||
type Signer interface {
|
||||
Sign(message io.Reader) ([]byte, error)
|
||||
}
|
||||
|
||||
func signObject(signer Signer, obj signableObject) ([]byte, error) {
|
||||
encoded := &plumbing.MemoryObject{}
|
||||
if err := obj.EncodeWithoutSignature(encoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := encoded.Reader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return signer.Sign(r)
|
||||
}
|
||||
80
vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go
generated
vendored
80
vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go
generated
vendored
@@ -29,6 +29,8 @@ type node struct {
|
||||
hash []byte
|
||||
children []noder.Noder
|
||||
isDir bool
|
||||
mode os.FileMode
|
||||
size int64
|
||||
}
|
||||
|
||||
// NewRootNode returns the root node based on a given billy.Filesystem.
|
||||
@@ -48,8 +50,15 @@ func NewRootNode(
|
||||
// difftree algorithm will detect changes in the contents of files and also in
|
||||
// their mode.
|
||||
//
|
||||
// Please note that the hash is calculated on first invocation of Hash(),
|
||||
// meaning that it will not update when the underlying file changes
|
||||
// between invocations.
|
||||
//
|
||||
// The hash of a directory is always a 24-bytes slice of zero values
|
||||
func (n *node) Hash() []byte {
|
||||
if n.hash == nil {
|
||||
n.calculateHash()
|
||||
}
|
||||
return n.hash
|
||||
}
|
||||
|
||||
@@ -121,81 +130,74 @@ func (n *node) calculateChildren() error {
|
||||
func (n *node) newChildNode(file os.FileInfo) (*node, error) {
|
||||
path := path.Join(n.path, file.Name())
|
||||
|
||||
hash, err := n.calculateHash(path, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node := &node{
|
||||
fs: n.fs,
|
||||
submodules: n.submodules,
|
||||
|
||||
path: path,
|
||||
hash: hash,
|
||||
isDir: file.IsDir(),
|
||||
size: file.Size(),
|
||||
mode: file.Mode(),
|
||||
}
|
||||
|
||||
if hash, isSubmodule := n.submodules[path]; isSubmodule {
|
||||
node.hash = append(hash[:], filemode.Submodule.Bytes()...)
|
||||
if _, isSubmodule := n.submodules[path]; isSubmodule {
|
||||
node.isDir = false
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (n *node) calculateHash(path string, file os.FileInfo) ([]byte, error) {
|
||||
if file.IsDir() {
|
||||
return make([]byte, 24), nil
|
||||
func (n *node) calculateHash() {
|
||||
if n.isDir {
|
||||
n.hash = make([]byte, 24)
|
||||
return
|
||||
}
|
||||
mode, err := filemode.NewFromOSFileMode(n.mode)
|
||||
if err != nil {
|
||||
n.hash = plumbing.ZeroHash[:]
|
||||
return
|
||||
}
|
||||
if submoduleHash, isSubmodule := n.submodules[n.path]; isSubmodule {
|
||||
n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...)
|
||||
return
|
||||
}
|
||||
|
||||
var hash plumbing.Hash
|
||||
var err error
|
||||
if file.Mode()&os.ModeSymlink != 0 {
|
||||
hash, err = n.doCalculateHashForSymlink(path, file)
|
||||
if n.mode&os.ModeSymlink != 0 {
|
||||
hash = n.doCalculateHashForSymlink()
|
||||
} else {
|
||||
hash, err = n.doCalculateHashForRegular(path, file)
|
||||
hash = n.doCalculateHashForRegular()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode, err := filemode.NewFromOSFileMode(file.Mode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(hash[:], mode.Bytes()...), nil
|
||||
n.hash = append(hash[:], mode.Bytes()...)
|
||||
}
|
||||
|
||||
func (n *node) doCalculateHashForRegular(path string, file os.FileInfo) (plumbing.Hash, error) {
|
||||
f, err := n.fs.Open(path)
|
||||
func (n *node) doCalculateHashForRegular() plumbing.Hash {
|
||||
f, err := n.fs.Open(n.path)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
h := plumbing.NewHasher(plumbing.BlobObject, file.Size())
|
||||
h := plumbing.NewHasher(plumbing.BlobObject, n.size)
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
|
||||
return h.Sum(), nil
|
||||
return h.Sum()
|
||||
}
|
||||
|
||||
func (n *node) doCalculateHashForSymlink(path string, file os.FileInfo) (plumbing.Hash, error) {
|
||||
target, err := n.fs.Readlink(path)
|
||||
func (n *node) doCalculateHashForSymlink() plumbing.Hash {
|
||||
target, err := n.fs.Readlink(n.path)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
|
||||
h := plumbing.NewHasher(plumbing.BlobObject, file.Size())
|
||||
h := plumbing.NewHasher(plumbing.BlobObject, n.size)
|
||||
if _, err := h.Write([]byte(target)); err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
|
||||
return h.Sum(), nil
|
||||
return h.Sum()
|
||||
}
|
||||
|
||||
func (n *node) String() string {
|
||||
|
||||
29
vendor/github.com/go-git/go-git/v5/worktree.go
generated
vendored
29
vendor/github.com/go-git/go-git/v5/worktree.go
generated
vendored
@@ -227,20 +227,17 @@ func (w *Worktree) createBranch(opts *CheckoutOptions) error {
|
||||
}
|
||||
|
||||
func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing.Hash, error) {
|
||||
if !opts.Hash.IsZero() {
|
||||
return opts.Hash, nil
|
||||
hash := opts.Hash
|
||||
if hash.IsZero() {
|
||||
b, err := w.r.Reference(opts.Branch, true)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
hash = b.Hash()
|
||||
}
|
||||
|
||||
b, err := w.r.Reference(opts.Branch, true)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
if !b.Name().IsTag() {
|
||||
return b.Hash(), nil
|
||||
}
|
||||
|
||||
o, err := w.r.Object(plumbing.AnyObject, b.Hash())
|
||||
o, err := w.r.Object(plumbing.AnyObject, hash)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
@@ -248,7 +245,7 @@ func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing
|
||||
switch o := o.(type) {
|
||||
case *object.Tag:
|
||||
if o.TargetType != plumbing.CommitObject {
|
||||
return plumbing.ZeroHash, fmt.Errorf("unsupported tag object target %q", o.TargetType)
|
||||
return plumbing.ZeroHash, fmt.Errorf("%w: tag target %q", object.ErrUnsupportedObject, o.TargetType)
|
||||
}
|
||||
|
||||
return o.Target, nil
|
||||
@@ -256,7 +253,7 @@ func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing
|
||||
return o.Hash, nil
|
||||
}
|
||||
|
||||
return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type())
|
||||
return plumbing.ZeroHash, fmt.Errorf("%w: %q", object.ErrUnsupportedObject, o.Type())
|
||||
}
|
||||
|
||||
func (w *Worktree) setHEADToCommit(commit plumbing.Hash) error {
|
||||
@@ -431,6 +428,10 @@ var worktreeDeny = map[string]struct{}{
|
||||
func validPath(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied {
|
||||
return fmt.Errorf("invalid path prefix: %q", p)
|
||||
}
|
||||
|
||||
69
vendor/github.com/go-git/go-git/v5/worktree_commit.go
generated
vendored
69
vendor/github.com/go-git/go-git/v5/worktree_commit.go
generated
vendored
@@ -3,6 +3,7 @@ package git
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/go-git/go-git/v5/storage"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
||||
@@ -43,29 +45,30 @@ func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
t, err := w.r.getTreeFromCommitHash(head.Hash())
|
||||
headCommit, err := w.r.CommitObject(head.Hash())
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
treeHash = t.Hash
|
||||
opts.Parents = []plumbing.Hash{head.Hash()}
|
||||
} else {
|
||||
idx, err := w.r.Storer.Index()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
opts.Parents = nil
|
||||
if len(headCommit.ParentHashes) != 0 {
|
||||
opts.Parents = []plumbing.Hash{headCommit.ParentHashes[0]}
|
||||
}
|
||||
}
|
||||
|
||||
h := &buildTreeHelper{
|
||||
fs: w.Filesystem,
|
||||
s: w.r.Storer,
|
||||
}
|
||||
idx, err := w.r.Storer.Index()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
treeHash, err = h.BuildTree(idx, opts)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
h := &buildTreeHelper{
|
||||
fs: w.Filesystem,
|
||||
s: w.r.Storer,
|
||||
}
|
||||
|
||||
treeHash, err = h.BuildTree(idx, opts)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
commit, err := w.buildCommitObject(msg, opts, treeHash)
|
||||
@@ -125,12 +128,17 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
|
||||
ParentHashes: opts.Parents,
|
||||
}
|
||||
|
||||
if opts.SignKey != nil {
|
||||
sig, err := w.buildCommitSignature(commit, opts.SignKey)
|
||||
// Convert SignKey into a Signer if set. Existing Signer should take priority.
|
||||
signer := opts.Signer
|
||||
if signer == nil && opts.SignKey != nil {
|
||||
signer = &gpgSigner{key: opts.SignKey}
|
||||
}
|
||||
if signer != nil {
|
||||
sig, err := signObject(signer, commit)
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
commit.PGPSignature = sig
|
||||
commit.PGPSignature = string(sig)
|
||||
}
|
||||
|
||||
obj := w.r.Storer.NewEncodedObject()
|
||||
@@ -140,20 +148,17 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
|
||||
return w.r.Storer.SetEncodedObject(obj)
|
||||
}
|
||||
|
||||
func (w *Worktree) buildCommitSignature(commit *object.Commit, signKey *openpgp.Entity) (string, error) {
|
||||
encoded := &plumbing.MemoryObject{}
|
||||
if err := commit.Encode(encoded); err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := encoded.Reader()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type gpgSigner struct {
|
||||
key *openpgp.Entity
|
||||
cfg *packet.Config
|
||||
}
|
||||
|
||||
func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
if err := openpgp.ArmoredDetachSign(&b, signKey, r, nil); err != nil {
|
||||
return "", err
|
||||
if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.String(), nil
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// buildTreeHelper converts a given index.Index file into multiple git objects
|
||||
@@ -263,4 +268,4 @@ func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tr
|
||||
return hash, nil
|
||||
}
|
||||
return h.s.SetEncodedObject(o)
|
||||
}
|
||||
}
|
||||
|
||||
27
vendor/github.com/go-git/go-git/v5/worktree_status.go
generated
vendored
27
vendor/github.com/go-git/go-git/v5/worktree_status.go
generated
vendored
@@ -271,7 +271,7 @@ func diffTreeIsEquals(a, b noder.Hasher) bool {
|
||||
// no error is returned. When path is a file, the blob.Hash is returned.
|
||||
func (w *Worktree) Add(path string) (plumbing.Hash, error) {
|
||||
// TODO(mcuadros): deprecate in favor of AddWithOption in v6.
|
||||
return w.doAdd(path, make([]gitignore.Pattern, 0))
|
||||
return w.doAdd(path, make([]gitignore.Pattern, 0), false)
|
||||
}
|
||||
|
||||
func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) {
|
||||
@@ -321,7 +321,7 @@ func (w *Worktree) AddWithOptions(opts *AddOptions) error {
|
||||
}
|
||||
|
||||
if opts.All {
|
||||
_, err := w.doAdd(".", w.Excludes)
|
||||
_, err := w.doAdd(".", w.Excludes, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -329,16 +329,11 @@ func (w *Worktree) AddWithOptions(opts *AddOptions) error {
|
||||
return w.AddGlob(opts.Glob)
|
||||
}
|
||||
|
||||
_, err := w.Add(opts.Path)
|
||||
_, err := w.doAdd(opts.Path, make([]gitignore.Pattern, 0), opts.SkipStatus)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern) (plumbing.Hash, error) {
|
||||
s, err := w.Status()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipStatus bool) (plumbing.Hash, error) {
|
||||
idx, err := w.r.Storer.Index()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, err
|
||||
@@ -348,6 +343,17 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern) (plumbi
|
||||
var added bool
|
||||
|
||||
fi, err := w.Filesystem.Lstat(path)
|
||||
|
||||
// status is required for doAddDirectory
|
||||
var s Status
|
||||
var err2 error
|
||||
if !skipStatus || fi == nil || fi.IsDir() {
|
||||
s, err2 = w.Status()
|
||||
if err2 != nil {
|
||||
return plumbing.ZeroHash, err2
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !fi.IsDir() {
|
||||
added, h, err = w.doAddFile(idx, s, path, ignorePattern)
|
||||
} else {
|
||||
@@ -421,8 +427,9 @@ func (w *Worktree) AddGlob(pattern string) error {
|
||||
|
||||
// doAddFile create a new blob from path and update the index, added is true if
|
||||
// the file added is different from the index.
|
||||
// if s status is nil will skip the status check and update the index anyway
|
||||
func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) {
|
||||
if s.File(path).Worktree == Unmodified {
|
||||
if s != nil && s.File(path).Worktree == Unmodified {
|
||||
return false, h, nil
|
||||
}
|
||||
if len(ignorePattern) > 0 {
|
||||
|
||||
13
vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go
generated
vendored
13
vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go
generated
vendored
@@ -34,8 +34,6 @@ const (
|
||||
DiffInsert Operation = 1
|
||||
// DiffEqual item represents an equal diff.
|
||||
DiffEqual Operation = 0
|
||||
//IndexSeparator is used to seperate the array indexes in an index string
|
||||
IndexSeparator = ","
|
||||
)
|
||||
|
||||
// Diff represents one diff operation
|
||||
@@ -406,14 +404,11 @@ func (dmp *DiffMatchPatch) DiffLinesToRunes(text1, text2 string) ([]rune, []rune
|
||||
func (dmp *DiffMatchPatch) DiffCharsToLines(diffs []Diff, lineArray []string) []Diff {
|
||||
hydrated := make([]Diff, 0, len(diffs))
|
||||
for _, aDiff := range diffs {
|
||||
chars := strings.Split(aDiff.Text, IndexSeparator)
|
||||
text := make([]string, len(chars))
|
||||
runes := []rune(aDiff.Text)
|
||||
text := make([]string, len(runes))
|
||||
|
||||
for i, r := range chars {
|
||||
i1, err := strconv.Atoi(r)
|
||||
if err == nil {
|
||||
text[i] = lineArray[i1]
|
||||
}
|
||||
for i, r := range runes {
|
||||
text[i] = lineArray[runeToInt(r)]
|
||||
}
|
||||
|
||||
aDiff.Text = strings.Join(text, "")
|
||||
|
||||
100
vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go
generated
vendored
100
vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go
generated
vendored
@@ -9,11 +9,16 @@
|
||||
package diffmatchpatch
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const UNICODE_INVALID_RANGE_START = 0xD800
|
||||
const UNICODE_INVALID_RANGE_END = 0xDFFF
|
||||
const UNICODE_INVALID_RANGE_DELTA = UNICODE_INVALID_RANGE_END - UNICODE_INVALID_RANGE_START + 1
|
||||
const UNICODE_RANGE_MAX = 0x10FFFF
|
||||
|
||||
// unescaper unescapes selected chars for compatibility with JavaScript's encodeURI.
|
||||
// In speed critical applications this could be dropped since the receiving application will certainly decode these fine. Note that this function is case-sensitive. Thus "%3F" would not be unescaped. But this is ok because it is only called with the output of HttpUtility.UrlEncode which returns lowercase hex. Example: "%3f" -> "?", "%24" -> "$", etc.
|
||||
var unescaper = strings.NewReplacer(
|
||||
@@ -93,14 +98,93 @@ func intArrayToString(ns []uint32) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
indexSeparator := IndexSeparator[0]
|
||||
|
||||
// Appr. 3 chars per num plus the comma.
|
||||
b := []byte{}
|
||||
b := []rune{}
|
||||
for _, n := range ns {
|
||||
b = strconv.AppendInt(b, int64(n), 10)
|
||||
b = append(b, indexSeparator)
|
||||
b = append(b, intToRune(n))
|
||||
}
|
||||
b = b[:len(b)-1]
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// These constants define the number of bits representable
|
||||
// in 1,2,3,4 byte utf8 sequences, respectively.
|
||||
const ONE_BYTE_BITS = 7
|
||||
const TWO_BYTE_BITS = 11
|
||||
const THREE_BYTE_BITS = 16
|
||||
const FOUR_BYTE_BITS = 21
|
||||
|
||||
// Helper for getting a sequence of bits from an integer.
|
||||
func getBits(i uint32, cnt byte, from byte) byte {
|
||||
return byte((i >> from) & ((1 << cnt) - 1))
|
||||
}
|
||||
|
||||
// Converts an integer in the range 0~1112060 into a rune.
|
||||
// Based on the ranges table in https://en.wikipedia.org/wiki/UTF-8
|
||||
func intToRune(i uint32) rune {
|
||||
if i < (1 << ONE_BYTE_BITS) {
|
||||
return rune(i)
|
||||
}
|
||||
|
||||
if i < (1 << TWO_BYTE_BITS) {
|
||||
r, size := utf8.DecodeRune([]byte{0b11000000 | getBits(i, 5, 6), 0b10000000 | getBits(i, 6, 0)})
|
||||
if size != 2 || r == utf8.RuneError {
|
||||
panic(fmt.Sprintf("Error encoding an int %d with size 2, got rune %v and size %d", size, r, i))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Last -3 here needed because for some reason 3rd to last codepoint 65533 in this range
|
||||
// was returning utf8.RuneError during encoding.
|
||||
if i < ((1 << THREE_BYTE_BITS) - UNICODE_INVALID_RANGE_DELTA - 3) {
|
||||
if i >= UNICODE_INVALID_RANGE_START {
|
||||
i += UNICODE_INVALID_RANGE_DELTA
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRune([]byte{0b11100000 | getBits(i, 4, 12), 0b10000000 | getBits(i, 6, 6), 0b10000000 | getBits(i, 6, 0)})
|
||||
if size != 3 || r == utf8.RuneError {
|
||||
panic(fmt.Sprintf("Error encoding an int %d with size 3, got rune %v and size %d", size, r, i))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
if i < (1<<FOUR_BYTE_BITS - UNICODE_INVALID_RANGE_DELTA - 3) {
|
||||
i += UNICODE_INVALID_RANGE_DELTA + 3
|
||||
r, size := utf8.DecodeRune([]byte{0b11110000 | getBits(i, 3, 18), 0b10000000 | getBits(i, 6, 12), 0b10000000 | getBits(i, 6, 6), 0b10000000 | getBits(i, 6, 0)})
|
||||
if size != 4 || r == utf8.RuneError {
|
||||
panic(fmt.Sprintf("Error encoding an int %d with size 4, got rune %v and size %d", size, r, i))
|
||||
}
|
||||
return r
|
||||
}
|
||||
panic(fmt.Sprintf("The integer %d is too large for runeToInt()", i))
|
||||
}
|
||||
|
||||
// Converts a rune generated by intToRune back to an integer
|
||||
func runeToInt(r rune) uint32 {
|
||||
i := uint32(r)
|
||||
if i < (1 << ONE_BYTE_BITS) {
|
||||
return i
|
||||
}
|
||||
|
||||
bytes := []byte{0, 0, 0, 0}
|
||||
|
||||
size := utf8.EncodeRune(bytes, r)
|
||||
|
||||
if size == 2 {
|
||||
return uint32(bytes[0]&0b11111)<<6 | uint32(bytes[1]&0b111111)
|
||||
}
|
||||
|
||||
if size == 3 {
|
||||
result := uint32(bytes[0]&0b1111)<<12 | uint32(bytes[1]&0b111111)<<6 | uint32(bytes[2]&0b111111)
|
||||
if result >= UNICODE_INVALID_RANGE_END {
|
||||
return result - UNICODE_INVALID_RANGE_DELTA
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if size == 4 {
|
||||
result := uint32(bytes[0]&0b111)<<18 | uint32(bytes[1]&0b111111)<<12 | uint32(bytes[2]&0b111111)<<6 | uint32(bytes[3]&0b111111)
|
||||
return result - UNICODE_INVALID_RANGE_DELTA - 3
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Unexpected state decoding rune=%v size=%d", r, size))
|
||||
}
|
||||
|
||||
2
vendor/github.com/skeema/knownhosts/NOTICE
generated
vendored
2
vendor/github.com/skeema/knownhosts/NOTICE
generated
vendored
@@ -1,4 +1,4 @@
|
||||
Copyright 2023 Skeema LLC and the Skeema Knownhosts authors
|
||||
Copyright 2024 Skeema LLC and the Skeema Knownhosts authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
2
vendor/github.com/skeema/knownhosts/README.md
generated
vendored
2
vendor/github.com/skeema/knownhosts/README.md
generated
vendored
@@ -100,7 +100,7 @@ config := &ssh.ClientConfig{
|
||||
|
||||
## License
|
||||
|
||||
**Source code copyright 2023 Skeema LLC and the Skeema Knownhosts authors**
|
||||
**Source code copyright 2024 Skeema LLC and the Skeema Knownhosts authors**
|
||||
|
||||
```text
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
14
vendor/github.com/skeema/knownhosts/knownhosts.go
generated
vendored
14
vendor/github.com/skeema/knownhosts/knownhosts.go
generated
vendored
@@ -76,13 +76,23 @@ func (hkcb HostKeyCallback) HostKeyAlgorithms(hostWithPort string) (algos []stri
|
||||
// example by https://github.com/golang/crypto/pull/254.
|
||||
hostKeys := hkcb.HostKeys(hostWithPort)
|
||||
seen := make(map[string]struct{}, len(hostKeys))
|
||||
for _, key := range hostKeys {
|
||||
typ := key.Type()
|
||||
addAlgo := func(typ string) {
|
||||
if _, already := seen[typ]; !already {
|
||||
algos = append(algos, typ)
|
||||
seen[typ] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, key := range hostKeys {
|
||||
typ := key.Type()
|
||||
if typ == ssh.KeyAlgoRSA {
|
||||
// KeyAlgoRSASHA256 and KeyAlgoRSASHA512 are only public key algorithms,
|
||||
// not public key formats, so they can't appear as a PublicKey.Type.
|
||||
// The corresponding PublicKey.Type is KeyAlgoRSA. See RFC 8332, Section 2.
|
||||
addAlgo(ssh.KeyAlgoRSASHA512)
|
||||
addAlgo(ssh.KeyAlgoRSASHA256)
|
||||
}
|
||||
addAlgo(typ)
|
||||
}
|
||||
return algos
|
||||
}
|
||||
|
||||
|
||||
39
vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go
generated
vendored
39
vendor/golang.org/x/crypto/internal/poly1305/bits_compat.go
generated
vendored
@@ -1,39 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.13
|
||||
|
||||
package poly1305
|
||||
|
||||
// Generic fallbacks for the math/bits intrinsics, copied from
|
||||
// src/math/bits/bits.go. They were added in Go 1.12, but Add64 and Sum64 had
|
||||
// variable time fallbacks until Go 1.13.
|
||||
|
||||
func bitsAdd64(x, y, carry uint64) (sum, carryOut uint64) {
|
||||
sum = x + y + carry
|
||||
carryOut = ((x & y) | ((x | y) &^ sum)) >> 63
|
||||
return
|
||||
}
|
||||
|
||||
func bitsSub64(x, y, borrow uint64) (diff, borrowOut uint64) {
|
||||
diff = x - y - borrow
|
||||
borrowOut = ((^x & y) | (^(x ^ y) & diff)) >> 63
|
||||
return
|
||||
}
|
||||
|
||||
func bitsMul64(x, y uint64) (hi, lo uint64) {
|
||||
const mask32 = 1<<32 - 1
|
||||
x0 := x & mask32
|
||||
x1 := x >> 32
|
||||
y0 := y & mask32
|
||||
y1 := y >> 32
|
||||
w0 := x0 * y0
|
||||
t := x1*y0 + w0>>32
|
||||
w1 := t & mask32
|
||||
w2 := t >> 32
|
||||
w1 += x0 * y1
|
||||
hi = x1*y1 + w2 + w1>>32
|
||||
lo = x * y
|
||||
return
|
||||
}
|
||||
21
vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go
generated
vendored
21
vendor/golang.org/x/crypto/internal/poly1305/bits_go1.13.go
generated
vendored
@@ -1,21 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
|
||||
package poly1305
|
||||
|
||||
import "math/bits"
|
||||
|
||||
func bitsAdd64(x, y, carry uint64) (sum, carryOut uint64) {
|
||||
return bits.Add64(x, y, carry)
|
||||
}
|
||||
|
||||
func bitsSub64(x, y, borrow uint64) (diff, borrowOut uint64) {
|
||||
return bits.Sub64(x, y, borrow)
|
||||
}
|
||||
|
||||
func bitsMul64(x, y uint64) (hi, lo uint64) {
|
||||
return bits.Mul64(x, y)
|
||||
}
|
||||
43
vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go
generated
vendored
43
vendor/golang.org/x/crypto/internal/poly1305/sum_generic.go
generated
vendored
@@ -7,7 +7,10 @@
|
||||
|
||||
package poly1305
|
||||
|
||||
import "encoding/binary"
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// Poly1305 [RFC 7539] is a relatively simple algorithm: the authentication tag
|
||||
// for a 64 bytes message is approximately
|
||||
@@ -114,13 +117,13 @@ type uint128 struct {
|
||||
}
|
||||
|
||||
func mul64(a, b uint64) uint128 {
|
||||
hi, lo := bitsMul64(a, b)
|
||||
hi, lo := bits.Mul64(a, b)
|
||||
return uint128{lo, hi}
|
||||
}
|
||||
|
||||
func add128(a, b uint128) uint128 {
|
||||
lo, c := bitsAdd64(a.lo, b.lo, 0)
|
||||
hi, c := bitsAdd64(a.hi, b.hi, c)
|
||||
lo, c := bits.Add64(a.lo, b.lo, 0)
|
||||
hi, c := bits.Add64(a.hi, b.hi, c)
|
||||
if c != 0 {
|
||||
panic("poly1305: unexpected overflow")
|
||||
}
|
||||
@@ -155,8 +158,8 @@ func updateGeneric(state *macState, msg []byte) {
|
||||
// hide leading zeroes. For full chunks, that's 1 << 128, so we can just
|
||||
// add 1 to the most significant (2¹²⁸) limb, h2.
|
||||
if len(msg) >= TagSize {
|
||||
h0, c = bitsAdd64(h0, binary.LittleEndian.Uint64(msg[0:8]), 0)
|
||||
h1, c = bitsAdd64(h1, binary.LittleEndian.Uint64(msg[8:16]), c)
|
||||
h0, c = bits.Add64(h0, binary.LittleEndian.Uint64(msg[0:8]), 0)
|
||||
h1, c = bits.Add64(h1, binary.LittleEndian.Uint64(msg[8:16]), c)
|
||||
h2 += c + 1
|
||||
|
||||
msg = msg[TagSize:]
|
||||
@@ -165,8 +168,8 @@ func updateGeneric(state *macState, msg []byte) {
|
||||
copy(buf[:], msg)
|
||||
buf[len(msg)] = 1
|
||||
|
||||
h0, c = bitsAdd64(h0, binary.LittleEndian.Uint64(buf[0:8]), 0)
|
||||
h1, c = bitsAdd64(h1, binary.LittleEndian.Uint64(buf[8:16]), c)
|
||||
h0, c = bits.Add64(h0, binary.LittleEndian.Uint64(buf[0:8]), 0)
|
||||
h1, c = bits.Add64(h1, binary.LittleEndian.Uint64(buf[8:16]), c)
|
||||
h2 += c
|
||||
|
||||
msg = nil
|
||||
@@ -219,9 +222,9 @@ func updateGeneric(state *macState, msg []byte) {
|
||||
m3 := h2r1
|
||||
|
||||
t0 := m0.lo
|
||||
t1, c := bitsAdd64(m1.lo, m0.hi, 0)
|
||||
t2, c := bitsAdd64(m2.lo, m1.hi, c)
|
||||
t3, _ := bitsAdd64(m3.lo, m2.hi, c)
|
||||
t1, c := bits.Add64(m1.lo, m0.hi, 0)
|
||||
t2, c := bits.Add64(m2.lo, m1.hi, c)
|
||||
t3, _ := bits.Add64(m3.lo, m2.hi, c)
|
||||
|
||||
// Now we have the result as 4 64-bit limbs, and we need to reduce it
|
||||
// modulo 2¹³⁰ - 5. The special shape of this Crandall prime lets us do
|
||||
@@ -243,14 +246,14 @@ func updateGeneric(state *macState, msg []byte) {
|
||||
|
||||
// To add c * 5 to h, we first add cc = c * 4, and then add (cc >> 2) = c.
|
||||
|
||||
h0, c = bitsAdd64(h0, cc.lo, 0)
|
||||
h1, c = bitsAdd64(h1, cc.hi, c)
|
||||
h0, c = bits.Add64(h0, cc.lo, 0)
|
||||
h1, c = bits.Add64(h1, cc.hi, c)
|
||||
h2 += c
|
||||
|
||||
cc = shiftRightBy2(cc)
|
||||
|
||||
h0, c = bitsAdd64(h0, cc.lo, 0)
|
||||
h1, c = bitsAdd64(h1, cc.hi, c)
|
||||
h0, c = bits.Add64(h0, cc.lo, 0)
|
||||
h1, c = bits.Add64(h1, cc.hi, c)
|
||||
h2 += c
|
||||
|
||||
// h2 is at most 3 + 1 + 1 = 5, making the whole of h at most
|
||||
@@ -287,9 +290,9 @@ func finalize(out *[TagSize]byte, h *[3]uint64, s *[2]uint64) {
|
||||
// in constant time, we compute t = h - (2¹³⁰ - 5), and select h as the
|
||||
// result if the subtraction underflows, and t otherwise.
|
||||
|
||||
hMinusP0, b := bitsSub64(h0, p0, 0)
|
||||
hMinusP1, b := bitsSub64(h1, p1, b)
|
||||
_, b = bitsSub64(h2, p2, b)
|
||||
hMinusP0, b := bits.Sub64(h0, p0, 0)
|
||||
hMinusP1, b := bits.Sub64(h1, p1, b)
|
||||
_, b = bits.Sub64(h2, p2, b)
|
||||
|
||||
// h = h if h < p else h - p
|
||||
h0 = select64(b, h0, hMinusP0)
|
||||
@@ -301,8 +304,8 @@ func finalize(out *[TagSize]byte, h *[3]uint64, s *[2]uint64) {
|
||||
//
|
||||
// by just doing a wide addition with the 128 low bits of h and discarding
|
||||
// the overflow.
|
||||
h0, c := bitsAdd64(h0, s[0], 0)
|
||||
h1, _ = bitsAdd64(h1, s[1], c)
|
||||
h0, c := bits.Add64(h0, s[0], 0)
|
||||
h1, _ = bits.Add64(h1, s[1], c)
|
||||
|
||||
binary.LittleEndian.PutUint64(out[0:8], h0)
|
||||
binary.LittleEndian.PutUint64(out[8:16], h1)
|
||||
|
||||
14
vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.s
generated
vendored
14
vendor/golang.org/x/crypto/internal/poly1305/sum_ppc64le.s
generated
vendored
@@ -19,15 +19,14 @@
|
||||
|
||||
#define POLY1305_MUL(h0, h1, h2, r0, r1, t0, t1, t2, t3, t4, t5) \
|
||||
MULLD r0, h0, t0; \
|
||||
MULLD r0, h1, t4; \
|
||||
MULHDU r0, h0, t1; \
|
||||
MULLD r0, h1, t4; \
|
||||
MULHDU r0, h1, t5; \
|
||||
ADDC t4, t1, t1; \
|
||||
MULLD r0, h2, t2; \
|
||||
ADDZE t5; \
|
||||
MULHDU r1, h0, t4; \
|
||||
MULLD r1, h0, h0; \
|
||||
ADD t5, t2, t2; \
|
||||
ADDE t5, t2, t2; \
|
||||
ADDC h0, t1, t1; \
|
||||
MULLD h2, r1, t3; \
|
||||
ADDZE t4, h0; \
|
||||
@@ -37,13 +36,11 @@
|
||||
ADDE t5, t3, t3; \
|
||||
ADDC h0, t2, t2; \
|
||||
MOVD $-4, t4; \
|
||||
MOVD t0, h0; \
|
||||
MOVD t1, h1; \
|
||||
ADDZE t3; \
|
||||
ANDCC $3, t2, h2; \
|
||||
AND t2, t4, t0; \
|
||||
RLDICL $0, t2, $62, h2; \
|
||||
AND t2, t4, h0; \
|
||||
ADDC t0, h0, h0; \
|
||||
ADDE t3, h1, h1; \
|
||||
ADDE t3, t1, h1; \
|
||||
SLD $62, t3, t4; \
|
||||
SRD $2, t2; \
|
||||
ADDZE h2; \
|
||||
@@ -75,6 +72,7 @@ TEXT ·update(SB), $0-32
|
||||
loop:
|
||||
POLY1305_ADD(R4, R8, R9, R10, R20, R21, R22)
|
||||
|
||||
PCALIGN $16
|
||||
multiply:
|
||||
POLY1305_MUL(R8, R9, R10, R11, R12, R16, R17, R18, R14, R20, R21)
|
||||
ADD $-16, R5
|
||||
|
||||
2
vendor/golang.org/x/sys/unix/aliases.go
generated
vendored
2
vendor/golang.org/x/sys/unix/aliases.go
generated
vendored
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos) && go1.9
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||
|
||||
package unix
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user