git-age/cmd/cmd.go

186 lines
3.6 KiB
Go

package cmd
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"filippo.io/age"
"filippo.io/age/agessh"
"github.com/bmatcuk/doublestar/v4"
"github.com/urfave/cli/v2"
)
var (
version = "develop"
debug = false
)
const REKEY = "GIT_AGE_REKEY"
func New() *cli.App {
app := cli.NewApp()
app.Name = "git-age"
app.Version = version
app.Usage = "Git encryption using age"
app.Commands = []*cli.Command{
Add,
Clean,
Identity,
Init,
Rekey,
Smudge,
TextConv,
}
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Debug mode",
Destination: &debug,
},
}
return app
}
func parseIdentityFile(file string) ([]age.Identity, error) {
fi, err := os.Open(file)
if err != nil {
return nil, err
}
defer fi.Close()
// First try age
i, err := age.ParseIdentities(fi)
if err != nil {
if debug {
fmt.Fprintf(os.Stderr, "could not parse %q as age identities, trying ssh next: %v\n", file, err)
}
ageErr := err
// Fall back to ssh
fi.Seek(0, 0)
content, err := io.ReadAll(fi)
if err != nil {
return nil, err
}
si, err := agessh.ParseIdentity(content)
if err != nil {
return nil, fmt.Errorf("could not parse identity file: age -> %w, ssh -> %w", ageErr, err)
}
i = []age.Identity{si}
}
return i, nil
}
func ageRecipients(file string) ([]age.Recipient, error) {
cfg, err := LoadConfig()
if err != nil {
return nil, err
}
for glob, val := range cfg {
match, err := doublestar.Match(glob, file)
if err != nil {
return nil, fmt.Errorf("bad glob %q: %w", glob, err)
}
if match {
return val.Recipients()
}
}
return nil, fmt.Errorf("no config found for %q", file)
}
var ErrNoIdentities = errors.New("no identities found")
func listIdentities() ([]string, error) {
out, err := cmd("git", "config", "--get-all", "git-age.identity")
if err != nil {
if out == "" {
return nil, ErrNoIdentities
}
return nil, err
}
return strings.Fields(out), nil
}
func ageIdentities() ([]age.Identity, error) {
keyFiles, err := listIdentities()
if err != nil {
return nil, err
}
var identities []age.Identity
for _, keyFile := range keyFiles {
i, err := parseIdentityFile(keyFile)
if err != nil {
return identities, err
}
identities = append(identities, i...)
}
return identities, nil
}
func gitBaseDir() (string, error) {
out, err := cmd("git", "rev-parse", "--show-toplevel")
if err != nil {
return "", err
}
stdout := strings.TrimSpace(string(out))
return stdout, nil
}
func gitRelPath(file string) (string, error) {
base, err := gitBaseDir()
if err != nil {
return "", err
}
apn, err := filepath.Abs(file)
if err != nil {
return "", err
}
return strings.TrimPrefix(apn, base+"/"), nil
}
func gitConfigDir(file string) (string, error) {
stdout, err := gitBaseDir()
if err != nil {
return "", err
}
normalFile := strings.ReplaceAll(file, string(filepath.Separator), "!")
dir := filepath.Join(stdout, ".git", "git-age", normalFile)
return dir, os.MkdirAll(dir, os.ModePerm)
}
func hasAttr(file string) (bool, error) {
out, err := cmd("git", "check-attr", "-a", file)
if err != nil {
return false, err
}
for _, line := range strings.Split(out, "\n") {
if line != "" && strings.Fields(line)[2] == "git-age" {
return true, nil
}
}
return false, nil
}
func cmd(command string, args ...string) (string, error) {
var stdout bytes.Buffer
c := exec.Command(command, args...)
c.Stdout = &stdout
if debug {
c.Stderr = os.Stderr
}
if err := c.Run(); err != nil {
return "", err
}
return stdout.String(), nil
}