mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 03:03:32 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8098dc1b9 | ||
|
|
e2aa2d3611 | ||
|
|
80e0e0d3ae |
@@ -57,9 +57,12 @@ The `cheat` command-line tool is organized into several key packages:
|
|||||||
### Command Layer (`cmd/cheat/`)
|
### Command Layer (`cmd/cheat/`)
|
||||||
- `main.go`: Entry point, cobra command definition, flag registration, command routing
|
- `main.go`: Entry point, cobra command definition, flag registration, command routing
|
||||||
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
||||||
- `completions.go`: Dynamic shell completion functions for cheatsheet names, tags, and paths
|
|
||||||
- Commands are routed via a `switch` block in the cobra `RunE` handler
|
- Commands are routed via a `switch` block in the cobra `RunE` handler
|
||||||
|
|
||||||
|
### Completions (`internal/completions/`)
|
||||||
|
- Dynamic shell completion functions for cheatsheet names, tags, and cheatpath names
|
||||||
|
- `generate.go`: Generates completion scripts for bash, zsh, fish, and powershell
|
||||||
|
|
||||||
### Core Internal Packages
|
### Core Internal Packages
|
||||||
|
|
||||||
1. **`internal/config`**: Configuration management
|
1. **`internal/config`**: Configuration management
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ The codebase follows consistent error handling patterns:
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
```go
|
```go
|
||||||
sheet, err := sheet.New(path, tags, false)
|
s, err := sheet.New(title, cheatpath, path, tags, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load sheet: %w", err)
|
return fmt.Errorf("failed to load sheet: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ On Windows, download the appropriate binary from the [releases page][releases],
|
|||||||
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
||||||
|
|
||||||
## Install via `go install`
|
## Install via `go install`
|
||||||
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
If you have `go` version `>=1.26` available on your `PATH`, you can install
|
||||||
`cheat` via `go install`:
|
`cheat` via `go install`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Accepted
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
In the `envVars()` function in `cmd/cheat/main.go`, the code parses environment variables assuming they all contain an equals sign:
|
In the `EnvVars()` function in `internal/config/env.go`, the code parses environment variables assuming they all contain an equals sign:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
for _, e := range os.Environ() {
|
for _, e := range os.Environ() {
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ multiple cheatpaths can configure them in `conf.yml`.
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- GitHub issue: #602
|
- GitHub issue: #602
|
||||||
- Implementation: `findLocalCheatpath()` in `internal/config/config.go`
|
- Implementation: `findLocalCheatpath()` in `internal/config/new.go`
|
||||||
|
|||||||
42
cmd/cheat/cmd_update.go
Normal file
42
cmd/cheat/cmd_update.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdUpdate updates git-backed cheatpaths.
|
||||||
|
func cmdUpdate(_ *cobra.Command, _ []string, conf config.Config) {
|
||||||
|
|
||||||
|
hasError := false
|
||||||
|
|
||||||
|
for _, path := range conf.Cheatpaths {
|
||||||
|
err := repo.Pull(path.Path)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
fmt.Printf("%s: ok\n", path.Name)
|
||||||
|
|
||||||
|
case errors.Is(err, git.ErrRepositoryNotExists):
|
||||||
|
fmt.Printf("%s: skipped (not a git repository)\n", path.Name)
|
||||||
|
|
||||||
|
case errors.Is(err, repo.ErrDirtyWorktree):
|
||||||
|
fmt.Printf("%s: skipped (dirty worktree)\n", path.Name)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: error (%v)\n", path.Name, err)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/cheat/cheat/internal/installer"
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "5.0.0"
|
const version = "5.1.0"
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "cheat [cheatsheet]",
|
Use: "cheat [cheatsheet]",
|
||||||
@@ -60,6 +60,9 @@ remember.`,
|
|||||||
To remove (delete) the foo/bar cheatsheet:
|
To remove (delete) the foo/bar cheatsheet:
|
||||||
cheat --rm foo/bar
|
cheat --rm foo/bar
|
||||||
|
|
||||||
|
To update all git-backed cheatpaths:
|
||||||
|
cheat --update
|
||||||
|
|
||||||
To view the configuration file path:
|
To view the configuration file path:
|
||||||
cheat --conf
|
cheat --conf
|
||||||
|
|
||||||
@@ -87,6 +90,7 @@ func init() {
|
|||||||
f.BoolP("list", "l", false, "List cheatsheets")
|
f.BoolP("list", "l", false, "List cheatsheets")
|
||||||
f.BoolP("regex", "r", false, "Treat search <phrase> as a regex")
|
f.BoolP("regex", "r", false, "Treat search <phrase> as a regex")
|
||||||
f.BoolP("tags", "T", false, "List all tags in use")
|
f.BoolP("tags", "T", false, "List all tags in use")
|
||||||
|
f.BoolP("update", "u", false, "Update git-backed cheatpaths")
|
||||||
f.BoolP("version", "v", false, "Print the version number")
|
f.BoolP("version", "v", false, "Print the version number")
|
||||||
f.Bool("conf", false, "Display the config file path")
|
f.Bool("conf", false, "Display the config file path")
|
||||||
|
|
||||||
@@ -221,6 +225,7 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
listFlag, _ := f.GetBool("list")
|
listFlag, _ := f.GetBool("list")
|
||||||
briefFlag, _ := f.GetBool("brief")
|
briefFlag, _ := f.GetBool("brief")
|
||||||
tagsFlag, _ := f.GetBool("tags")
|
tagsFlag, _ := f.GetBool("tags")
|
||||||
|
updateFlag, _ := f.GetBool("update")
|
||||||
tagVal, _ := f.GetString("tag")
|
tagVal, _ := f.GetString("tag")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -239,6 +244,9 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
case tagsFlag:
|
case tagsFlag:
|
||||||
cmdTags(cmd, args, conf)
|
cmdTags(cmd, args, conf)
|
||||||
|
|
||||||
|
case updateFlag:
|
||||||
|
cmdUpdate(cmd, args, conf)
|
||||||
|
|
||||||
case f.Changed("search"):
|
case f.Changed("search"):
|
||||||
cmdSearch(cmd, args, conf)
|
cmdSearch(cmd, args, conf)
|
||||||
|
|
||||||
|
|||||||
23
doc/cheat.1
23
doc/cheat.1
@@ -53,6 +53,9 @@ Filter only to sheets tagged with \f[I]TAG\f[R].
|
|||||||
\-T, \[en]tags
|
\-T, \[en]tags
|
||||||
List all tags in use.
|
List all tags in use.
|
||||||
.TP
|
.TP
|
||||||
|
\-u, \[en]update
|
||||||
|
Update git\-backed cheatpaths by pulling the latest changes.
|
||||||
|
.TP
|
||||||
\-v, \[en]version
|
\-v, \[en]version
|
||||||
Print the version number.
|
Print the version number.
|
||||||
.TP
|
.TP
|
||||||
@@ -98,23 +101,31 @@ cheat \-c \-r \-s \f[I]`(?:[0\-9]{1,3}.){3}[0\-9]{1,3}'\f[R]
|
|||||||
To remove (delete) the foo/bar cheatsheet:
|
To remove (delete) the foo/bar cheatsheet:
|
||||||
cheat \[en]rm \f[I]foo/bar\f[R]
|
cheat \[en]rm \f[I]foo/bar\f[R]
|
||||||
.TP
|
.TP
|
||||||
|
To update all git\-backed cheatpaths:
|
||||||
|
cheat \[en]update
|
||||||
|
.TP
|
||||||
|
To update only the `community' cheatpath:
|
||||||
|
cheat \-u \-p \f[I]community\f[R]
|
||||||
|
.TP
|
||||||
To view the configuration file path:
|
To view the configuration file path:
|
||||||
cheat \[en]conf
|
cheat \[en]conf
|
||||||
.SH FILES
|
.SH FILES
|
||||||
.SS Configuration
|
.SS Configuration
|
||||||
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
||||||
named \f[I]conf.yaml\f[R].
|
named \f[I]conf.yml\f[R].
|
||||||
\f[B]cheat\f[R] will search for \f[I]conf.yaml\f[R] in varying
|
\f[B]cheat\f[R] will search for \f[I]conf.yml\f[R] in varying locations,
|
||||||
locations, depending upon your platform:
|
depending upon your platform:
|
||||||
.SS Linux, OSX, and other Unixes
|
.SS Linux, OSX, and other Unixes
|
||||||
.IP "1." 3
|
.IP "1." 3
|
||||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||||
.IP "2." 3
|
.IP "2." 3
|
||||||
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yaml
|
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yml
|
||||||
.IP "3." 3
|
.IP "3." 3
|
||||||
\f[B]$HOME\f[R]/.config/cheat/conf.yml
|
\f[B]$HOME\f[R]/.config/cheat/conf.yml
|
||||||
.IP "4." 3
|
.IP "4." 3
|
||||||
\f[B]$HOME\f[R]/.cheat/conf.yml
|
\f[B]$HOME\f[R]/.cheat/conf.yml
|
||||||
|
.IP "5." 3
|
||||||
|
/etc/cheat/conf.yml
|
||||||
.SS Windows
|
.SS Windows
|
||||||
.IP "1." 3
|
.IP "1." 3
|
||||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||||
@@ -124,7 +135,7 @@ locations, depending upon your platform:
|
|||||||
\f[B]PROGRAMDATA\f[R]/cheat/conf.yml
|
\f[B]PROGRAMDATA\f[R]/cheat/conf.yml
|
||||||
.PP
|
.PP
|
||||||
\f[B]cheat\f[R] will search in the order specified above.
|
\f[B]cheat\f[R] will search in the order specified above.
|
||||||
The first \f[I]conf.yaml\f[R] encountered will be respected.
|
The first \f[I]conf.yml\f[R] encountered will be respected.
|
||||||
.PP
|
.PP
|
||||||
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]d
|
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]d
|
||||||
like to generate one automatically.
|
like to generate one automatically.
|
||||||
@@ -134,7 +145,7 @@ location for your platform.
|
|||||||
.SS Cheatpaths
|
.SS Cheatpaths
|
||||||
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
||||||
are the directories in which cheatsheets are stored.
|
are the directories in which cheatsheets are stored.
|
||||||
Cheatpaths may be configured in \f[I]conf.yaml\f[R], and viewed via
|
Cheatpaths may be configured in \f[I]conf.yml\f[R], and viewed via
|
||||||
\f[B]cheat \-d\f[R].
|
\f[B]cheat \-d\f[R].
|
||||||
.PP
|
.PP
|
||||||
For detailed instructions on how to configure cheatpaths, please refer
|
For detailed instructions on how to configure cheatpaths, please refer
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ OPTIONS
|
|||||||
-T, --tags
|
-T, --tags
|
||||||
: List all tags in use.
|
: List all tags in use.
|
||||||
|
|
||||||
|
-u, --update
|
||||||
|
: Update git-backed cheatpaths by pulling the latest changes.
|
||||||
|
|
||||||
-v, --version
|
-v, --version
|
||||||
: Print the version number.
|
: Print the version number.
|
||||||
|
|
||||||
@@ -106,6 +109,12 @@ To search (by regex) for cheatsheets that contain an IP address:
|
|||||||
To remove (delete) the foo/bar cheatsheet:
|
To remove (delete) the foo/bar cheatsheet:
|
||||||
: cheat --rm _foo/bar_
|
: cheat --rm _foo/bar_
|
||||||
|
|
||||||
|
To update all git-backed cheatpaths:
|
||||||
|
: cheat --update
|
||||||
|
|
||||||
|
To update only the 'community' cheatpath:
|
||||||
|
: cheat -u -p _community_
|
||||||
|
|
||||||
To view the configuration file path:
|
To view the configuration file path:
|
||||||
: cheat --conf
|
: cheat --conf
|
||||||
|
|
||||||
@@ -116,15 +125,16 @@ FILES
|
|||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
**cheat** is configured via a YAML file that is conventionally named
|
**cheat** is configured via a YAML file that is conventionally named
|
||||||
_conf.yaml_. **cheat** will search for _conf.yaml_ in varying locations,
|
_conf.yml_. **cheat** will search for _conf.yml_ in varying locations,
|
||||||
depending upon your platform:
|
depending upon your platform:
|
||||||
|
|
||||||
### Linux, OSX, and other Unixes ###
|
### Linux, OSX, and other Unixes ###
|
||||||
|
|
||||||
1. **CHEAT_CONFIG_PATH**
|
1. **CHEAT_CONFIG_PATH**
|
||||||
2. **XDG_CONFIG_HOME**/cheat/conf.yaml
|
2. **XDG_CONFIG_HOME**/cheat/conf.yml
|
||||||
3. **$HOME**/.config/cheat/conf.yml
|
3. **$HOME**/.config/cheat/conf.yml
|
||||||
4. **$HOME**/.cheat/conf.yml
|
4. **$HOME**/.cheat/conf.yml
|
||||||
|
5. /etc/cheat/conf.yml
|
||||||
|
|
||||||
### Windows ###
|
### Windows ###
|
||||||
|
|
||||||
@@ -132,7 +142,7 @@ depending upon your platform:
|
|||||||
2. **APPDATA**/cheat/conf.yml
|
2. **APPDATA**/cheat/conf.yml
|
||||||
3. **PROGRAMDATA**/cheat/conf.yml
|
3. **PROGRAMDATA**/cheat/conf.yml
|
||||||
|
|
||||||
**cheat** will search in the order specified above. The first _conf.yaml_
|
**cheat** will search in the order specified above. The first _conf.yml_
|
||||||
encountered will be respected.
|
encountered will be respected.
|
||||||
|
|
||||||
If **cheat** cannot locate a config file, it will ask if you'd like to generate
|
If **cheat** cannot locate a config file, it will ask if you'd like to generate
|
||||||
@@ -144,7 +154,7 @@ for your platform.
|
|||||||
Cheatpaths
|
Cheatpaths
|
||||||
----------
|
----------
|
||||||
**cheat** reads its cheatsheets from "cheatpaths", which are the directories in
|
**cheat** reads its cheatsheets from "cheatpaths", which are the directories in
|
||||||
which cheatsheets are stored. Cheatpaths may be configured in _conf.yaml_, and
|
which cheatsheets are stored. Cheatpaths may be configured in _conf.yml_, and
|
||||||
viewed via **cheat -d**.
|
viewed via **cheat -d**.
|
||||||
|
|
||||||
For detailed instructions on how to configure cheatpaths, please refer to the
|
For detailed instructions on how to configure cheatpaths, please refer to the
|
||||||
|
|||||||
130
internal/repo/pull.go
Normal file
130
internal/repo/pull.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
|
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrDirtyWorktree indicates that the worktree has uncommitted changes.
|
||||||
|
var ErrDirtyWorktree = errors.New("dirty worktree")
|
||||||
|
|
||||||
|
// Pull performs a git pull on the repository at path. It returns
|
||||||
|
// ErrDirtyWorktree if the worktree has uncommitted changes, and
|
||||||
|
// git.ErrRepositoryNotExists if path is not a git repository.
|
||||||
|
func Pull(path string) error {
|
||||||
|
|
||||||
|
// open the repository
|
||||||
|
r, err := git.PlainOpen(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the worktree
|
||||||
|
wt, err := r.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the worktree is clean
|
||||||
|
status, err := wt.Status()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !status.IsClean() {
|
||||||
|
return ErrDirtyWorktree
|
||||||
|
}
|
||||||
|
|
||||||
|
// build pull options, using SSH auth when the remote is SSH
|
||||||
|
opts := &git.PullOptions{}
|
||||||
|
if auth, err := sshAuth(r); err == nil && auth != nil {
|
||||||
|
opts.Auth = auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull
|
||||||
|
err = wt.Pull(opts)
|
||||||
|
if errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultKeyFiles are the SSH key filenames tried in order, matching the
|
||||||
|
// default behavior of OpenSSH.
|
||||||
|
var defaultKeyFiles = []string{
|
||||||
|
"id_rsa",
|
||||||
|
"id_ecdsa",
|
||||||
|
"id_ecdsa_sk",
|
||||||
|
"id_ed25519",
|
||||||
|
"id_ed25519_sk",
|
||||||
|
"id_dsa",
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshAuth returns an appropriate SSH auth method if the origin remote uses
|
||||||
|
// the SSH protocol, or nil if it does not. It tries the SSH agent first, then
|
||||||
|
// falls back to default key files in ~/.ssh/.
|
||||||
|
func sshAuth(r *git.Repository) (transport.AuthMethod, error) {
|
||||||
|
remote, err := r.Remote("origin")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := remote.Config().URLs
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ep, err := transport.NewEndpoint(urls[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep.Protocol != "ssh" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := ep.User
|
||||||
|
if user == "" {
|
||||||
|
user = "git"
|
||||||
|
}
|
||||||
|
|
||||||
|
// try default key files first — this is more reliable than the SSH
|
||||||
|
// agent, which may report success even when no keys are loaded
|
||||||
|
home, err := homedir.Dir()
|
||||||
|
if err == nil {
|
||||||
|
if auth := findKeyFile(filepath.Join(home, ".ssh"), user); auth != nil {
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to SSH agent
|
||||||
|
if auth, err := gitssh.NewSSHAgentAuth(user); err == nil {
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findKeyFile looks for a usable SSH private key in sshDir, trying the
|
||||||
|
// standard OpenSSH default filenames in order. Returns nil if no usable key
|
||||||
|
// is found.
|
||||||
|
func findKeyFile(sshDir, user string) transport.AuthMethod {
|
||||||
|
for _, name := range defaultKeyFiles {
|
||||||
|
keyPath := filepath.Join(sshDir, name)
|
||||||
|
if _, err := os.Stat(keyPath); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
auth, err := gitssh.NewPublicKeysFromFile(user, keyPath, "")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
315
internal/repo/pull_test.go
Normal file
315
internal/repo/pull_test.go
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
gitconfig "github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testCommitOpts returns a CommitOptions suitable for test commits.
|
||||||
|
func testCommitOpts() *git.CommitOptions {
|
||||||
|
return &git.CommitOptions{
|
||||||
|
Author: &object.Signature{
|
||||||
|
Name: "test",
|
||||||
|
Email: "test@test",
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initBareRepoWithCommit creates a bare git repository at dir with an initial
|
||||||
|
// commit, suitable for use as a remote.
|
||||||
|
func initBareRepoWithCommit(t *testing.T, dir string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// init a non-bare repo to make the commit, then we'll clone it as bare
|
||||||
|
tmpWork := t.TempDir()
|
||||||
|
r, err := git.PlainInit(tmpWork, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := filepath.Join(tmpWork, "README")
|
||||||
|
if err := os.WriteFile(f, []byte("hello\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wt, err := r.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get worktree: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := wt.Add("README"); err != nil {
|
||||||
|
t.Fatalf("failed to stage file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = wt.Commit("initial commit", testCommitOpts()); err != nil {
|
||||||
|
t.Fatalf("failed to commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone as bare into the target dir
|
||||||
|
if _, err = git.PlainClone(dir, true, &git.CloneOptions{URL: tmpWork}); err != nil {
|
||||||
|
t.Fatalf("failed to create bare clone: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloneLocal clones the bare repo at bareDir into a new working directory and
|
||||||
|
// returns the path.
|
||||||
|
func cloneLocal(t *testing.T, bareDir string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||||
|
URL: bareDir,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to clone: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushNewCommit clones bareDir into a temporary working copy, commits a new
|
||||||
|
// file, and pushes back to the bare repo.
|
||||||
|
func pushNewCommit(t *testing.T, bareDir string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tmpWork := t.TempDir()
|
||||||
|
r, err := git.PlainClone(tmpWork, false, &git.CloneOptions{URL: bareDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to clone for push: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpWork, "new.txt"), []byte("new\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wt, err := r.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get worktree: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := wt.Add("new.txt"); err != nil {
|
||||||
|
t.Fatalf("failed to stage file: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := wt.Commit("add new file", testCommitOpts()); err != nil {
|
||||||
|
t.Fatalf("failed to commit: %v", err)
|
||||||
|
}
|
||||||
|
if err := r.Push(&git.PushOptions{}); err != nil {
|
||||||
|
t.Fatalf("failed to push: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTestKey creates an unencrypted ed25519 PEM private key file at path.
|
||||||
|
func generateTestKey(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
_, priv, err := ed25519.GenerateKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
|
||||||
|
if err := os.WriteFile(path, pemBytes, 0600); err != nil {
|
||||||
|
t.Fatalf("failed to write key file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pull tests ---
|
||||||
|
|
||||||
|
func TestPull_NotARepo(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
err := Pull(dir)
|
||||||
|
if err != git.ErrRepositoryNotExists {
|
||||||
|
t.Fatalf("expected ErrRepositoryNotExists, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPull_CleanAlreadyUpToDate(t *testing.T) {
|
||||||
|
bare := t.TempDir()
|
||||||
|
initBareRepoWithCommit(t, bare)
|
||||||
|
clone := cloneLocal(t, bare)
|
||||||
|
|
||||||
|
err := Pull(clone)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil (already up-to-date), got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPull_NewUpstreamChanges(t *testing.T) {
|
||||||
|
bare := t.TempDir()
|
||||||
|
initBareRepoWithCommit(t, bare)
|
||||||
|
clone := cloneLocal(t, bare)
|
||||||
|
|
||||||
|
// push a new commit to the bare repo after the clone
|
||||||
|
pushNewCommit(t, bare)
|
||||||
|
|
||||||
|
err := Pull(clone)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil (successful pull), got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the new file was pulled
|
||||||
|
if _, err := os.Stat(filepath.Join(clone, "new.txt")); err != nil {
|
||||||
|
t.Fatalf("expected new.txt to exist after pull: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPull_DirtyWorktree(t *testing.T) {
|
||||||
|
bare := t.TempDir()
|
||||||
|
initBareRepoWithCommit(t, bare)
|
||||||
|
clone := cloneLocal(t, bare)
|
||||||
|
|
||||||
|
// make the worktree dirty with a modified tracked file
|
||||||
|
if err := os.WriteFile(filepath.Join(clone, "README"), []byte("changed\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to modify file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Pull(clone)
|
||||||
|
if err != ErrDirtyWorktree {
|
||||||
|
t.Fatalf("expected ErrDirtyWorktree, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPull_DirtyWorktreeUntracked(t *testing.T) {
|
||||||
|
bare := t.TempDir()
|
||||||
|
initBareRepoWithCommit(t, bare)
|
||||||
|
clone := cloneLocal(t, bare)
|
||||||
|
|
||||||
|
// make the worktree dirty with an untracked file
|
||||||
|
if err := os.WriteFile(filepath.Join(clone, "untracked.txt"), []byte("new\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to create file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Pull(clone)
|
||||||
|
if err != ErrDirtyWorktree {
|
||||||
|
t.Fatalf("expected ErrDirtyWorktree, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- sshAuth tests ---
|
||||||
|
|
||||||
|
func TestSshAuth_NonSSHRemote(t *testing.T) {
|
||||||
|
bare := t.TempDir()
|
||||||
|
initBareRepoWithCommit(t, bare)
|
||||||
|
clone := cloneLocal(t, bare)
|
||||||
|
|
||||||
|
r, err := git.PlainOpen(clone)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the clone's origin is a local file:// path, not SSH
|
||||||
|
auth, err := sshAuth(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil error, got: %v", err)
|
||||||
|
}
|
||||||
|
if auth != nil {
|
||||||
|
t.Fatalf("expected nil auth for non-SSH remote, got: %v", auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSshAuth_NoRemote(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
r, err := git.PlainInit(dir, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// repo has no remotes
|
||||||
|
auth, err := sshAuth(r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for missing remote, got auth: %v", auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSshAuth_SSHRemote(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
r, err := git.PlainInit(dir, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init repo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add an SSH remote
|
||||||
|
_, err = r.CreateRemote(&gitconfig.RemoteConfig{
|
||||||
|
Name: "origin",
|
||||||
|
URLs: []string{"git@github.com:example/repo.git"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create remote: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sshAuth should not return an error — even if no key is found, it
|
||||||
|
// returns (nil, nil) rather than an error
|
||||||
|
auth, err := sshAuth(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can't predict whether auth is nil or non-nil here because it
|
||||||
|
// depends on whether the test runner has SSH keys or an agent; just
|
||||||
|
// verify it didn't error
|
||||||
|
_ = auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- findKeyFile tests ---
|
||||||
|
|
||||||
|
func TestFindKeyFile_ValidKey(t *testing.T) {
|
||||||
|
sshDir := t.TempDir()
|
||||||
|
generateTestKey(t, filepath.Join(sshDir, "id_ed25519"))
|
||||||
|
|
||||||
|
auth := findKeyFile(sshDir, "git")
|
||||||
|
if auth == nil {
|
||||||
|
t.Fatal("expected non-nil auth for valid key file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindKeyFile_NoKeys(t *testing.T) {
|
||||||
|
sshDir := t.TempDir()
|
||||||
|
|
||||||
|
auth := findKeyFile(sshDir, "git")
|
||||||
|
if auth != nil {
|
||||||
|
t.Fatalf("expected nil auth for empty directory, got: %v", auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindKeyFile_InvalidKey(t *testing.T) {
|
||||||
|
sshDir := t.TempDir()
|
||||||
|
// write garbage into a file named like a key
|
||||||
|
if err := os.WriteFile(filepath.Join(sshDir, "id_ed25519"), []byte("not a key"), 0600); err != nil {
|
||||||
|
t.Fatalf("failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := findKeyFile(sshDir, "git")
|
||||||
|
if auth != nil {
|
||||||
|
t.Fatalf("expected nil auth for invalid key file, got: %v", auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindKeyFile_SkipsInvalidFindsValid(t *testing.T) {
|
||||||
|
sshDir := t.TempDir()
|
||||||
|
|
||||||
|
// put garbage in id_rsa (tried first), valid key in id_ed25519 (tried later)
|
||||||
|
if err := os.WriteFile(filepath.Join(sshDir, "id_rsa"), []byte("not a key"), 0600); err != nil {
|
||||||
|
t.Fatalf("failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
generateTestKey(t, filepath.Join(sshDir, "id_ed25519"))
|
||||||
|
|
||||||
|
auth := findKeyFile(sshDir, "git")
|
||||||
|
if auth == nil {
|
||||||
|
t.Fatal("expected non-nil auth; should skip invalid id_rsa and find id_ed25519")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
pull() {
|
|
||||||
for d in `cheat -d | awk '{print $2}'`;
|
|
||||||
do
|
|
||||||
echo "Update $d"
|
|
||||||
cd "$d"
|
|
||||||
[ -d ".git" ] && git pull || :
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Finished update"
|
|
||||||
}
|
|
||||||
|
|
||||||
push() {
|
|
||||||
for d in `cheat -d | grep -v "community" | awk '{print $2}'`;
|
|
||||||
do
|
|
||||||
cd "$d"
|
|
||||||
if [ -d ".git" ]
|
|
||||||
then
|
|
||||||
echo "Push modifications $d"
|
|
||||||
files=$(git ls-files -mo | tr '\n' ' ')
|
|
||||||
git add -A && git commit -m "Edited files: $files" && git push || :
|
|
||||||
else
|
|
||||||
echo "$(pwd) is not a git managed folder"
|
|
||||||
echo "First connect this to your personal git repository"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Finished push operation"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$1" = "pull" ]; then
|
|
||||||
pull
|
|
||||||
elif [ "$1" = "push" ]; then
|
|
||||||
push
|
|
||||||
else
|
|
||||||
echo "Usage:
|
|
||||||
# pull changes
|
|
||||||
cheatsheets pull
|
|
||||||
|
|
||||||
# push changes
|
|
||||||
cheatsheets push"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user