mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-09 09:08:50 +00:00
Compare commits
5 Commits
11a690b1c6
...
v0.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df130a822 | ||
|
|
e04861fcee | ||
|
|
73d80203a0 | ||
|
|
c3cb59a94a | ||
|
|
59edaba5b8 |
@@ -111,7 +111,7 @@ var decrypt = &cli.Command{
|
||||
}
|
||||
plaintext, err := nip04.Decrypt(ciphertext, ss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||
return fmt.Errorf("failed to decrypt as nip04: %w", err)
|
||||
}
|
||||
stdout(plaintext)
|
||||
}
|
||||
|
||||
642
git.go
642
git.go
@@ -12,73 +12,11 @@ import (
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip34"
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
type Nip34Config struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Web []string `json:"web"`
|
||||
Owner string `json:"owner"`
|
||||
GraspServers []string `json:"grasp-servers"`
|
||||
EarliestUniqueCommit string `json:"earliest-unique-commit"`
|
||||
Maintainers []string `json:"maintainers"`
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) Validate() error {
|
||||
_, err := parsePubKey(localConfig.Owner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err)
|
||||
}
|
||||
|
||||
for _, maintainer := range localConfig.Maintainers {
|
||||
_, err := parsePubKey(maintainer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("maintainer pubkey '%s' is not valid: %w", maintainer, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) ToRepository() nip34.Repository {
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
localRepo := nip34.Repository{
|
||||
ID: localConfig.Identifier,
|
||||
Name: localConfig.Name,
|
||||
Description: localConfig.Description,
|
||||
Web: localConfig.Web,
|
||||
EarliestUniqueCommitID: localConfig.EarliestUniqueCommit,
|
||||
Maintainers: []nostr.PubKey{},
|
||||
Event: nostr.Event{
|
||||
PubKey: owner,
|
||||
},
|
||||
}
|
||||
for _, server := range localConfig.GraspServers {
|
||||
graspServerURL := nostr.NormalizeURL(server)
|
||||
url := fmt.Sprintf("http%s/%s/%s.git",
|
||||
graspServerURL[2:], nip19.EncodeNpub(localRepo.PubKey), localConfig.Identifier)
|
||||
localRepo.Clone = append(localRepo.Clone, url)
|
||||
localRepo.Relays = append(localRepo.Relays, graspServerURL)
|
||||
}
|
||||
for _, maintainer := range localConfig.Maintainers {
|
||||
pk, err := parsePubKey(maintainer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
localRepo.Maintainers = append(localRepo.Maintainers, pk)
|
||||
}
|
||||
|
||||
return localRepo
|
||||
}
|
||||
|
||||
var git = &cli.Command{
|
||||
Name: "git",
|
||||
Usage: "git-related operations",
|
||||
@@ -89,16 +27,7 @@ aside from those, there is also:
|
||||
- 'nak git sync' for getting the latest metadata update from nostr relays (called automatically by other commands)
|
||||
`,
|
||||
Commands: []*cli.Command{
|
||||
gitInit,
|
||||
gitSync,
|
||||
gitClone,
|
||||
gitPush,
|
||||
gitPull,
|
||||
gitFetch,
|
||||
},
|
||||
}
|
||||
|
||||
var gitInit = &cli.Command{
|
||||
{
|
||||
Name: "init",
|
||||
Usage: "initialize a nip34 repository configuration",
|
||||
Flags: []cli.Flag{
|
||||
@@ -272,84 +201,18 @@ var gitInit = &cli.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func promptForConfig(config *Nip34Config) error {
|
||||
rlConfig := &readline.Config{
|
||||
Stdout: os.Stderr,
|
||||
InterruptPrompt: "^C",
|
||||
DisableAutoSaveHistory: true,
|
||||
}
|
||||
|
||||
rl, err := readline.NewEx(rlConfig)
|
||||
if err != nil {
|
||||
},
|
||||
{
|
||||
Name: "sync",
|
||||
Usage: "sync repository with relays",
|
||||
Flags: defaultKeyFlags,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||
_, _, err := gitSync(ctx, kr)
|
||||
return err
|
||||
}
|
||||
defer rl.Close()
|
||||
|
||||
promptString := func(currentVal *string, prompt string) error {
|
||||
rl.SetPrompt(color.YellowString("%s [%s]: ", prompt, *currentVal))
|
||||
answer, err := rl.Readline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
answer = strings.TrimSpace(answer)
|
||||
if answer != "" {
|
||||
*currentVal = answer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
promptSlice := func(currentVal *[]string, prompt string) error {
|
||||
defaultStr := strings.Join(*currentVal, ", ")
|
||||
rl.SetPrompt(color.YellowString("%s (comma-separated) [%s]: ", prompt, defaultStr))
|
||||
answer, err := rl.Readline()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
answer = strings.TrimSpace(answer)
|
||||
if answer != "" {
|
||||
parts := strings.Split(answer, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if trimmed := strings.TrimSpace(p); trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
*currentVal = result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log("\nenter repository details (press Enter to keep default):\n\n")
|
||||
|
||||
if err := promptString(&config.Identifier, "identifier"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptString(&config.Name, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptString(&config.Description, "description"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptString(&config.Owner, "owner (npub or hex)"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptSlice(&config.GraspServers, "grasp servers"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptSlice(&config.Web, "web URLs"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := promptSlice(&config.Maintainers, "other maintainers"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log("\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
var gitClone = &cli.Command{
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clone",
|
||||
Usage: "clone a NIP-34 repository from a nostr:// URI",
|
||||
Description: `the <repository> parameter maybe in the form "<npub, hex, nprofile or nip05>/<identifier>", ngit-style like "nostr://<npub>/<relay>/<identifier>" or an "naddr1..." code.`,
|
||||
@@ -439,7 +302,7 @@ var gitClone = &cli.Command{
|
||||
fetchFromRemotes(ctx, targetDir, repo)
|
||||
|
||||
// if we have a state with a HEAD, try to reset to it
|
||||
if state.Event.ID != nostr.ZeroID && state.HEAD != "" {
|
||||
if state != nil && state.HEAD != "" {
|
||||
if headCommit, ok := state.Branches[state.HEAD]; ok {
|
||||
// check if we have that commit
|
||||
checkCmd := exec.Command("git", "cat-file", "-e", headCommit)
|
||||
@@ -458,16 +321,15 @@ var gitClone = &cli.Command{
|
||||
}
|
||||
|
||||
// update refs from state
|
||||
if state.Event.ID != nostr.ZeroID {
|
||||
gitUpdateRefs(ctx, targetDir, state)
|
||||
if state != nil {
|
||||
gitUpdateRefs(ctx, targetDir, *state)
|
||||
}
|
||||
|
||||
log("cloned into %s\n", color.GreenString(targetDir))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var gitPush = &cli.Command{
|
||||
},
|
||||
{
|
||||
Name: "push",
|
||||
Usage: "push git changes",
|
||||
Flags: append(defaultKeyFlags, &cli.BoolFlag{
|
||||
@@ -488,7 +350,7 @@ var gitPush = &cli.Command{
|
||||
log("publishing as %s\n", color.CyanString(currentNpub))
|
||||
|
||||
// sync to ensure everything is up to date
|
||||
repo, state, err := syncRepository(ctx, kr)
|
||||
repo, state, err := gitSync(ctx, kr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sync: %w", err)
|
||||
}
|
||||
@@ -504,10 +366,6 @@ var gitPush = &cli.Command{
|
||||
return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk))
|
||||
}
|
||||
|
||||
if state.Event.ID != nostr.ZeroID {
|
||||
logverbose("found state event: %s\n", state.Event.ID)
|
||||
}
|
||||
|
||||
// get commit for the local branch
|
||||
res, err := exec.Command("git", "rev-parse", localBranch).Output()
|
||||
if err != nil {
|
||||
@@ -518,8 +376,8 @@ var gitPush = &cli.Command{
|
||||
logverbose("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit)
|
||||
|
||||
// create a new state if we didn't find any
|
||||
if state.Event.ID == nostr.ZeroID {
|
||||
state = nip34.RepositoryState{
|
||||
if state == nil {
|
||||
state = &nip34.RepositoryState{
|
||||
ID: repo.ID,
|
||||
Branches: make(map[string]string),
|
||||
Tags: make(map[string]string),
|
||||
@@ -587,13 +445,12 @@ var gitPush = &cli.Command{
|
||||
return fmt.Errorf("failed to push to any remote")
|
||||
}
|
||||
|
||||
gitUpdateRefs(ctx, "", state)
|
||||
gitUpdateRefs(ctx, "", *state)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var gitPull = &cli.Command{
|
||||
},
|
||||
{
|
||||
Name: "pull",
|
||||
Usage: "pull git changes",
|
||||
Flags: []cli.Flag{
|
||||
@@ -601,10 +458,22 @@ var gitPull = &cli.Command{
|
||||
Name: "rebase",
|
||||
Usage: "rebase instead of merge",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ff-only",
|
||||
Usage: "only allow fast-forward merges",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ff",
|
||||
Usage: "allow fast-forward merges",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-ff",
|
||||
Usage: "always perform a merge instead of fast-forwarding",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
// sync to fetch latest state and metadata
|
||||
_, state, err := syncRepository(ctx, nil)
|
||||
_, state, err := gitSync(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sync: %w", err)
|
||||
}
|
||||
@@ -631,8 +500,51 @@ var gitPull = &cli.Command{
|
||||
return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit)
|
||||
}
|
||||
|
||||
// merge or rebase
|
||||
// determine merge strategy
|
||||
var strategy string
|
||||
strategiesSpecified := 0
|
||||
if c.Bool("rebase") {
|
||||
strategy = "rebase"
|
||||
strategiesSpecified++
|
||||
}
|
||||
if c.Bool("ff-only") {
|
||||
strategy = "ff-only"
|
||||
strategiesSpecified++
|
||||
}
|
||||
if c.Bool("no-ff") {
|
||||
strategy = "no-ff"
|
||||
strategiesSpecified++
|
||||
}
|
||||
if c.Bool("ff") {
|
||||
strategy = "ff"
|
||||
strategiesSpecified++
|
||||
}
|
||||
|
||||
if strategiesSpecified > 1 {
|
||||
return fmt.Errorf("flags --rebase, --ff-only, --ff, --no-ff are mutually exclusive")
|
||||
}
|
||||
|
||||
if strategy == "" {
|
||||
// check git config for pull.rebase
|
||||
cmd := exec.Command("git", "config", "--get", "pull.rebase")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && strings.TrimSpace(string(output)) == "true" {
|
||||
strategy = "rebase"
|
||||
} else if err == nil && strings.TrimSpace(string(output)) == "false" {
|
||||
strategy = "ff"
|
||||
} else {
|
||||
// check git config for pull.ff
|
||||
cmd := exec.Command("git", "config", "--get", "pull.ff")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && strings.TrimSpace(string(output)) == "only" {
|
||||
strategy = "ff-only"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute the merge or rebase
|
||||
switch strategy {
|
||||
case "rebase":
|
||||
log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit))
|
||||
rebaseCmd := exec.Command("git", "rebase", targetCommit)
|
||||
rebaseCmd.Stderr = os.Stderr
|
||||
@@ -640,61 +552,291 @@ var gitPull = &cli.Command{
|
||||
if err := rebaseCmd.Run(); err != nil {
|
||||
return fmt.Errorf("rebase failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
log("merging %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch))
|
||||
mergeCmd := exec.Command("git", "merge", targetCommit)
|
||||
case "ff-only":
|
||||
log("pulling %s into %s (fast-forward only)...\n", color.CyanString(targetCommit), color.CyanString(localBranch))
|
||||
mergeCmd := exec.Command("git", "merge", "--ff-only", targetCommit)
|
||||
mergeCmd.Stderr = os.Stderr
|
||||
mergeCmd.Stdout = os.Stdout
|
||||
if err := mergeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("merge failed: %w", err)
|
||||
}
|
||||
case "no-ff":
|
||||
log("pulling %s into %s (no fast-forward)...\n", color.CyanString(targetCommit), color.CyanString(localBranch))
|
||||
mergeCmd := exec.Command("git", "merge", "--no-ff", targetCommit)
|
||||
mergeCmd.Stderr = os.Stderr
|
||||
mergeCmd.Stdout = os.Stdout
|
||||
if err := mergeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("merge failed: %w", err)
|
||||
}
|
||||
case "ff":
|
||||
log("pulling %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch))
|
||||
mergeCmd := exec.Command("git", "merge", "--ff", targetCommit)
|
||||
mergeCmd.Stderr = os.Stderr
|
||||
mergeCmd.Stdout = os.Stdout
|
||||
if err := mergeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("merge failed: %w", err)
|
||||
}
|
||||
default:
|
||||
// get current commit
|
||||
res, err := exec.Command("git", "rev-parse", localBranch).Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current commit for branch %s: %w", localBranch, err)
|
||||
}
|
||||
currentCommit := strings.TrimSpace(string(res))
|
||||
|
||||
// check if fast-forward possible
|
||||
cmd := exec.Command("git", "merge-base", "--is-ancestor", currentCommit, targetCommit)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("fast-forward merge not possible, specify --rebase, --ff-only, --ff, or --no-ff; or use git config")
|
||||
}
|
||||
|
||||
// do fast-forward
|
||||
log("fast-forwarding to %s...\n", color.CyanString(targetCommit))
|
||||
mergeCmd := exec.Command("git", "merge", "--ff-only", targetCommit)
|
||||
mergeCmd.Stderr = os.Stderr
|
||||
mergeCmd.Stdout = os.Stdout
|
||||
if err := mergeCmd.Run(); err != nil {
|
||||
return fmt.Errorf("fast-forward failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log("pull complete\n")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var gitFetch = &cli.Command{
|
||||
},
|
||||
{
|
||||
Name: "fetch",
|
||||
Usage: "fetch git data",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
_, _, err := syncRepository(ctx, nil)
|
||||
_, _, err := gitSync(ctx, nil)
|
||||
return err
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var gitSync = &cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "sync repository with relays",
|
||||
Flags: defaultKeyFlags,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||
_, _, err := syncRepository(ctx, kr)
|
||||
return err
|
||||
},
|
||||
func promptForStringList(
|
||||
name string,
|
||||
existing []string,
|
||||
defaults []string,
|
||||
normalize func(string) string,
|
||||
validate func(string) bool,
|
||||
) ([]string, error) {
|
||||
options := make([]string, 0, len(defaults)+len(existing)+1)
|
||||
options = append(options, defaults...)
|
||||
options = append(options, "add another")
|
||||
|
||||
// add existing not in options
|
||||
for _, item := range existing {
|
||||
if !slices.Contains(options, item) {
|
||||
options = append(options, item)
|
||||
}
|
||||
}
|
||||
|
||||
func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository, nip34.RepositoryState, error) {
|
||||
selected := make([]string, len(existing))
|
||||
copy(selected, existing)
|
||||
|
||||
for {
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: name,
|
||||
Options: options,
|
||||
Default: selected,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if slices.Contains(selected, "add another") {
|
||||
selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" })
|
||||
|
||||
singular := name
|
||||
if strings.HasSuffix(singular, "s") {
|
||||
singular = singular[:len(singular)-1]
|
||||
}
|
||||
|
||||
newPrompt := &survey.Input{
|
||||
Message: fmt.Sprintf("enter new %s", singular),
|
||||
}
|
||||
var newItem string
|
||||
if err := survey.AskOne(newPrompt, &newItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if newItem != "" {
|
||||
if normalize != nil {
|
||||
newItem = normalize(newItem)
|
||||
}
|
||||
if validate != nil && !validate(newItem) {
|
||||
// invalid, ask again
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(options, newItem) {
|
||||
options = append(options, newItem)
|
||||
// swap to put "add another" at end
|
||||
options[len(options)-1], options[len(options)-2] = options[len(options)-2], options[len(options)-1]
|
||||
}
|
||||
if !slices.Contains(selected, newItem) {
|
||||
selected = append(selected, newItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func promptForConfig(config *Nip34Config) error {
|
||||
log("\nenter repository details (use arrow keys to navigate, space to select/deselect, enter to confirm):\n\n")
|
||||
|
||||
// prompt for identifier
|
||||
identifierPrompt := &survey.Input{
|
||||
Message: "identifier",
|
||||
Default: config.Identifier,
|
||||
}
|
||||
if err := survey.AskOne(identifierPrompt, &config.Identifier); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prompt for name
|
||||
namePrompt := &survey.Input{
|
||||
Message: "name",
|
||||
Default: config.Name,
|
||||
}
|
||||
if err := survey.AskOne(namePrompt, &config.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prompt for description
|
||||
descPrompt := &survey.Input{
|
||||
Message: "description",
|
||||
Default: config.Description,
|
||||
}
|
||||
if err := survey.AskOne(descPrompt, &config.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prompt for owner
|
||||
for {
|
||||
ownerPrompt := &survey.Input{
|
||||
Message: "owner (npub or hex)",
|
||||
Default: config.Owner,
|
||||
}
|
||||
if err := survey.AskOne(ownerPrompt, &config.Owner); err != nil {
|
||||
return err
|
||||
}
|
||||
if pubkey, err := parsePubKey(config.Owner); err == nil {
|
||||
config.Owner = pubkey.Hex()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// prompt for grasp servers
|
||||
graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{
|
||||
"gitnostr.com",
|
||||
"relay.ngit.dev",
|
||||
"pyramid.fiatjaf.com",
|
||||
"git.shakespeare.dyi",
|
||||
}, graspServerHost, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.GraspServers = graspServers
|
||||
|
||||
// prompt for web URLs
|
||||
webURLs, err := promptForStringList("web URLs", config.Web, []string{
|
||||
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
|
||||
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
|
||||
config.Identifier,
|
||||
),
|
||||
}, func(s string) string {
|
||||
return "http" + nostr.NormalizeURL(s)[2:]
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Web = webURLs
|
||||
|
||||
// Prompt for maintainers
|
||||
maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool {
|
||||
pk, err := parsePubKey(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if pk.Hex() == config.Owner {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Maintainers = maintainers
|
||||
|
||||
log("\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) {
|
||||
// read current nip34.json
|
||||
localConfig, err := readNip34ConfigFile("")
|
||||
if err != nil {
|
||||
return nip34.Repository{}, nip34.RepositoryState{}, err
|
||||
return nip34.Repository{}, nil, err
|
||||
}
|
||||
|
||||
// parse owner
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
if err != nil {
|
||||
return nip34.Repository{}, nip34.RepositoryState{}, fmt.Errorf("invalid owner public key: %w", err)
|
||||
return nip34.Repository{}, nil, fmt.Errorf("invalid owner public key: %w", err)
|
||||
}
|
||||
|
||||
// fetch repository announcement and state from relays
|
||||
repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
||||
if err != nil && repo.Event.ID == nostr.ZeroID {
|
||||
log("couldn't fetch repository metadata (%s), will publish now\n", err)
|
||||
// create a local repository object from config and publish it
|
||||
localRepo := localConfig.ToRepository()
|
||||
|
||||
if signer != nil {
|
||||
signerPk, err := signer.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
logverbose("failed to fetch repository metadata: %v\n", err)
|
||||
// create a local repository object from config
|
||||
repo = localConfig.ToRepository()
|
||||
return repo, nil, fmt.Errorf("failed to get signer pubkey: %w", err)
|
||||
}
|
||||
if signerPk != owner {
|
||||
return repo, nil, fmt.Errorf("provided signer pubkey does not match owner, can't publish repository")
|
||||
} else {
|
||||
event := localRepo.ToEvent()
|
||||
if err := signer.SignEvent(ctx, &event); err != nil {
|
||||
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
||||
}
|
||||
|
||||
relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...)
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||
if res.Error != nil {
|
||||
log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
||||
} else {
|
||||
log("> published to %s\n", color.GreenString(res.RelayURL))
|
||||
}
|
||||
}
|
||||
repo = localRepo
|
||||
}
|
||||
} else {
|
||||
return repo, nil, fmt.Errorf("no signer provided to publish repository (run 'nak git sync' with the '--sec' flag)")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
if _, ok := err.(StateErr); ok {
|
||||
// some error with the state, just do nothing and proceed
|
||||
} else {
|
||||
// actually fail with this error we don't know about
|
||||
return repo, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// check if local config differs from remote announcement
|
||||
// construct local repo from config for comparison
|
||||
localRepo := localConfig.ToRepository()
|
||||
@@ -762,8 +904,8 @@ func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository,
|
||||
fetchFromRemotes(ctx, "", repo)
|
||||
|
||||
// update refs from state
|
||||
if state.Event.ID != nostr.ZeroID {
|
||||
gitUpdateRefs(ctx, "", state)
|
||||
if state != nil {
|
||||
gitUpdateRefs(ctx, "", *state)
|
||||
}
|
||||
|
||||
return repo, state, nil
|
||||
@@ -800,9 +942,12 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
|
||||
|
||||
// delete all nip34/grasp/ remotes
|
||||
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, remote := range remotes {
|
||||
for i, remote := range remotes {
|
||||
remote = strings.TrimSpace(remote)
|
||||
remotes[i] = remote
|
||||
|
||||
if strings.HasPrefix(remote, "nip34/grasp/") {
|
||||
if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) {
|
||||
delCmd := exec.Command("git", "remote", "remove", remote)
|
||||
if dir != "" {
|
||||
delCmd.Dir = dir
|
||||
@@ -812,18 +957,18 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create new remotes for each grasp server
|
||||
for _, relay := range repo.Relays {
|
||||
relayURL := nostr.NormalizeURL(relay)
|
||||
remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://")
|
||||
remoteName = strings.TrimPrefix(remoteName, "ws://")
|
||||
remote := "nip34/grasp/" + strings.TrimPrefix(relay, "wss://")
|
||||
|
||||
if !slices.Contains(remotes, remote) {
|
||||
// construct the git URL
|
||||
gitURL := fmt.Sprintf("http%s/%s/%s.git",
|
||||
relayURL[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
|
||||
relay[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
|
||||
|
||||
addCmd := exec.Command("git", "remote", "add", remoteName, gitURL)
|
||||
addCmd := exec.Command("git", "remote", "add", remote, gitURL)
|
||||
if dir != "" {
|
||||
addCmd.Dir = dir
|
||||
}
|
||||
@@ -832,7 +977,8 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
stderr = string(exiterr.Stderr)
|
||||
}
|
||||
logverbose("failed to add remote %s: %s %s\n", remoteName, stderr, string(out))
|
||||
logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -895,7 +1041,7 @@ func fetchRepositoryAndState(
|
||||
pubkey nostr.PubKey,
|
||||
identifier string,
|
||||
relayHints []string,
|
||||
) (repo nip34.Repository, state nip34.RepositoryState, err error) {
|
||||
) (repo nip34.Repository, state *nip34.RepositoryState, err error) {
|
||||
// fetch repository announcement (30617)
|
||||
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
@@ -905,7 +1051,7 @@ func fetchRepositoryAndState(
|
||||
"d": []string{identifier},
|
||||
},
|
||||
Limit: 2,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) {
|
||||
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
|
||||
if ie.Event.CreatedAt > repo.CreatedAt {
|
||||
repo = nip34.ParseRepository(ie.Event)
|
||||
}
|
||||
@@ -915,8 +1061,7 @@ func fetchRepositoryAndState(
|
||||
}
|
||||
|
||||
// fetch repository state (30618)
|
||||
var stateFound bool
|
||||
var stateErr error
|
||||
var stateErr *StateErr
|
||||
for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{30618},
|
||||
Authors: []nostr.PubKey{pubkey},
|
||||
@@ -924,26 +1069,23 @@ func fetchRepositoryAndState(
|
||||
"d": []string{identifier},
|
||||
},
|
||||
Limit: 2,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) {
|
||||
if ie.Event.CreatedAt > state.CreatedAt {
|
||||
state = nip34.ParseRepositoryState(ie.Event)
|
||||
stateFound = true
|
||||
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
|
||||
if state == nil || ie.Event.CreatedAt > state.CreatedAt {
|
||||
state_ := nip34.ParseRepositoryState(ie.Event)
|
||||
|
||||
if state.HEAD == "" {
|
||||
stateErr = fmt.Errorf("state is missing HEAD")
|
||||
if state_.HEAD == "" {
|
||||
stateErr = &StateErr{"state is missing HEAD"}
|
||||
continue
|
||||
}
|
||||
if _, ok := state.Branches[state.HEAD]; !ok {
|
||||
stateErr = fmt.Errorf("state is missing commit for HEAD branch '%s'", state.HEAD)
|
||||
if _, ok := state_.Branches[state_.HEAD]; !ok {
|
||||
stateErr = &StateErr{fmt.Sprintf("state is missing commit for HEAD branch '%s'", state_.HEAD)}
|
||||
continue
|
||||
}
|
||||
|
||||
stateErr = nil
|
||||
state = &state_
|
||||
}
|
||||
}
|
||||
if !stateFound {
|
||||
return repo, state, fmt.Errorf("no repository state (kind 30618) found")
|
||||
}
|
||||
if stateErr != nil {
|
||||
return repo, state, stateErr
|
||||
}
|
||||
@@ -951,6 +1093,10 @@ func fetchRepositoryAndState(
|
||||
return repo, state, nil
|
||||
}
|
||||
|
||||
type StateErr struct{ string }
|
||||
|
||||
func (s StateErr) Error() string { return string(s.string) }
|
||||
|
||||
func findGitRoot(startDir string) string {
|
||||
if startDir == "" {
|
||||
var err error
|
||||
@@ -1007,7 +1153,7 @@ func readNip34ConfigFile(baseDir string) (Nip34Config, error) {
|
||||
|
||||
// normalize grasp relay URLs
|
||||
for i := range localConfig.GraspServers {
|
||||
localConfig.GraspServers[i] = nostr.NormalizeURL(localConfig.GraspServers[i])
|
||||
localConfig.GraspServers[i] = graspServerHost(localConfig.GraspServers[i])
|
||||
}
|
||||
|
||||
if err := localConfig.Validate(); err != nil {
|
||||
@@ -1220,3 +1366,67 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) (
|
||||
|
||||
return localBranch, remoteBranch, nil
|
||||
}
|
||||
|
||||
func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] }
|
||||
|
||||
type Nip34Config struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Web []string `json:"web"`
|
||||
Owner string `json:"owner"`
|
||||
GraspServers []string `json:"grasp-servers"`
|
||||
EarliestUniqueCommit string `json:"earliest-unique-commit"`
|
||||
Maintainers []string `json:"maintainers"`
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) Validate() error {
|
||||
_, err := parsePubKey(localConfig.Owner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err)
|
||||
}
|
||||
|
||||
for _, maintainer := range localConfig.Maintainers {
|
||||
_, err := parsePubKey(maintainer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("maintainer pubkey '%s' is not valid: %w", maintainer, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (localConfig Nip34Config) ToRepository() nip34.Repository {
|
||||
owner, err := parsePubKey(localConfig.Owner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
localRepo := nip34.Repository{
|
||||
ID: localConfig.Identifier,
|
||||
Name: localConfig.Name,
|
||||
Description: localConfig.Description,
|
||||
Web: localConfig.Web,
|
||||
EarliestUniqueCommitID: localConfig.EarliestUniqueCommit,
|
||||
Maintainers: []nostr.PubKey{},
|
||||
Event: nostr.Event{
|
||||
PubKey: owner,
|
||||
},
|
||||
}
|
||||
for _, server := range localConfig.GraspServers {
|
||||
graspServerURL := nostr.NormalizeURL(server)
|
||||
url := fmt.Sprintf("http%s/%s/%s.git",
|
||||
graspServerURL[2:], nip19.EncodeNpub(localRepo.PubKey), localConfig.Identifier)
|
||||
localRepo.Clone = append(localRepo.Clone, url)
|
||||
localRepo.Relays = append(localRepo.Relays, graspServerURL)
|
||||
}
|
||||
for _, maintainer := range localConfig.Maintainers {
|
||||
pk, err := parsePubKey(maintainer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
localRepo.Maintainers = append(localRepo.Maintainers, pk)
|
||||
}
|
||||
|
||||
return localRepo
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -28,6 +28,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
@@ -53,10 +54,12 @@ require (
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
||||
31
go.sum
31
go.sum
@@ -2,10 +2,13 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||
fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 h1:R16mnlJ3qvVar7G4rzY+Z+mEAf2O6wpHTlRlHAt2Od8=
|
||||
fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8/go.mod h1:QEGyTgAjjTFwDx2BJGZiCdmoAcWA/G+sQy7wDqKzSPU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
@@ -53,6 +56,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -108,6 +112,7 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||
@@ -118,6 +123,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
@@ -137,14 +144,18 @@ github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzr
|
||||
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
||||
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -210,27 +221,36 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -238,17 +258,28 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
||||
Reference in New Issue
Block a user