mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-10 09:28:51 +00:00
git: move things around, allow for nil state as a possible value, fix syncing when repository is not announced yet.
This commit is contained in:
839
git.go
839
git.go
@@ -17,68 +17,6 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"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{
|
var git = &cli.Command{
|
||||||
Name: "git",
|
Name: "git",
|
||||||
Usage: "git-related operations",
|
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)
|
- 'nak git sync' for getting the latest metadata update from nostr relays (called automatically by other commands)
|
||||||
`,
|
`,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
gitInit,
|
{
|
||||||
gitSync,
|
|
||||||
gitClone,
|
|
||||||
gitPush,
|
|
||||||
gitPull,
|
|
||||||
gitFetch,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var gitInit = &cli.Command{
|
|
||||||
Name: "init",
|
Name: "init",
|
||||||
Usage: "initialize a nip34 repository configuration",
|
Usage: "initialize a nip34 repository configuration",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@@ -272,6 +201,325 @@ var gitInit = &cli.Command{
|
|||||||
|
|
||||||
return nil
|
return 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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.`,
|
||||||
|
ArgsUsage: "<repository> [directory]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args()
|
||||||
|
if args.Len() == 0 {
|
||||||
|
return fmt.Errorf("missing repository address")
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, identifier, relayHints, err := parseRepositoryAddress(ctx, args.Get(0))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch repository metadata and state
|
||||||
|
repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine target directory
|
||||||
|
targetDir := ""
|
||||||
|
if args.Len() >= 2 {
|
||||||
|
targetDir = args.Get(1)
|
||||||
|
} else {
|
||||||
|
targetDir = repo.ID
|
||||||
|
}
|
||||||
|
if targetDir == "" {
|
||||||
|
targetDir = repo.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// if targetDir exists and is non-empty, bail
|
||||||
|
if fi, err := os.Stat(targetDir); err == nil && fi.IsDir() {
|
||||||
|
entries, err := os.ReadDir(targetDir)
|
||||||
|
if err == nil && len(entries) > 0 {
|
||||||
|
return fmt.Errorf("target directory '%s' already exists and is not empty", targetDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create directory
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory '%s': %w", targetDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize git inside the directory
|
||||||
|
initCmd := exec.Command("git", "init")
|
||||||
|
initCmd.Dir = targetDir
|
||||||
|
if err := initCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize git repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write nip34.json inside cloned directory
|
||||||
|
localConfig := Nip34Config{
|
||||||
|
Identifier: repo.ID,
|
||||||
|
Name: repo.Name,
|
||||||
|
Description: repo.Description,
|
||||||
|
Web: repo.Web,
|
||||||
|
Owner: nip19.EncodeNpub(repo.Event.PubKey),
|
||||||
|
GraspServers: make([]string, 0, len(repo.Relays)),
|
||||||
|
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
|
||||||
|
Maintainers: make([]string, 0, len(repo.Maintainers)),
|
||||||
|
}
|
||||||
|
for _, r := range repo.Relays {
|
||||||
|
localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r))
|
||||||
|
}
|
||||||
|
for _, m := range repo.Maintainers {
|
||||||
|
localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := localConfig.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write nip34.json
|
||||||
|
if err := writeNip34ConfigFile(targetDir, localConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add nip34.json to .git/info/exclude in cloned repo
|
||||||
|
excludeNip34ConfigFile(targetDir)
|
||||||
|
|
||||||
|
// setup git remotes
|
||||||
|
gitSetupRemotes(ctx, targetDir, repo)
|
||||||
|
|
||||||
|
// fetch from each grasp remote
|
||||||
|
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 headCommit, ok := state.Branches[state.HEAD]; ok {
|
||||||
|
// check if we have that commit
|
||||||
|
checkCmd := exec.Command("git", "cat-file", "-e", headCommit)
|
||||||
|
checkCmd.Dir = targetDir
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
// commit exists, reset to it
|
||||||
|
log("resetting to commit %s...\n", color.CyanString(headCommit))
|
||||||
|
resetCmd := exec.Command("git", "reset", "--hard", headCommit)
|
||||||
|
resetCmd.Dir = targetDir
|
||||||
|
resetCmd.Stderr = os.Stderr
|
||||||
|
if err := resetCmd.Run(); err != nil {
|
||||||
|
log("! failed to reset: %v\n", color.YellowString("%v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update refs from state
|
||||||
|
if state != nil {
|
||||||
|
gitUpdateRefs(ctx, targetDir, *state)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("cloned into %s\n", color.GreenString(targetDir))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "push",
|
||||||
|
Usage: "push git changes",
|
||||||
|
Flags: append(defaultKeyFlags, &cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "force push to git remotes",
|
||||||
|
}),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// setup signer
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to gather keyer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log publishing as npub
|
||||||
|
currentPk, _ := kr.GetPublicKey(ctx)
|
||||||
|
currentNpub := nip19.EncodeNpub(currentPk)
|
||||||
|
log("publishing as %s\n", color.CyanString(currentNpub))
|
||||||
|
|
||||||
|
// sync to ensure everything is up to date
|
||||||
|
repo, state, err := gitSync(ctx, kr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out which branches to push
|
||||||
|
localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if signer matches owner or is in maintainers
|
||||||
|
if currentPk != repo.Event.PubKey && !slices.Contains(repo.Maintainers, currentPk) {
|
||||||
|
return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk))
|
||||||
|
}
|
||||||
|
|
||||||
|
// get commit for the local branch
|
||||||
|
res, err := exec.Command("git", "rev-parse", localBranch).Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err)
|
||||||
|
}
|
||||||
|
currentCommit := strings.TrimSpace(string(res))
|
||||||
|
|
||||||
|
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 == nil {
|
||||||
|
state = &nip34.RepositoryState{
|
||||||
|
ID: repo.ID,
|
||||||
|
Branches: make(map[string]string),
|
||||||
|
Tags: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the branch
|
||||||
|
if !c.Bool("force") {
|
||||||
|
if prevCommit, exists := state.Branches[remoteBranch]; exists {
|
||||||
|
// check if prevCommit is an ancestor of currentCommit (fast-forward check)
|
||||||
|
cmd := exec.Command("git", "merge-base", "--is-ancestor", prevCommit, currentCommit)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("non-fast-forward push not allowed, use --force to override")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.Branches[remoteBranch] = currentCommit
|
||||||
|
log("- setting branch %s to commit %s\n", color.CyanString(remoteBranch), color.CyanString(currentCommit))
|
||||||
|
|
||||||
|
// set the HEAD to the local branch if none is set
|
||||||
|
if state.HEAD == "" {
|
||||||
|
state.HEAD = remoteBranch
|
||||||
|
log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch))
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and sign the new state event
|
||||||
|
newStateEvent := state.ToEvent()
|
||||||
|
err = kr.SignEvent(ctx, &newStateEvent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error signing state event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("- publishing updated repository state to " + color.CyanString("%v", repo.Relays) + "\n")
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, repo.Relays, newStateEvent) {
|
||||||
|
if res.Error != nil {
|
||||||
|
log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
||||||
|
} else {
|
||||||
|
log("> published to %s\n", color.GreenString(res.RelayURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push to each grasp remote
|
||||||
|
pushSuccesses := 0
|
||||||
|
for _, relay := range repo.Relays {
|
||||||
|
relayURL := nostr.NormalizeURL(relay)
|
||||||
|
remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://")
|
||||||
|
remoteName = strings.TrimPrefix(remoteName, "ws://")
|
||||||
|
|
||||||
|
log("pushing to %s...\n", color.CyanString(remoteName))
|
||||||
|
pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)}
|
||||||
|
if c.Bool("force") {
|
||||||
|
pushArgs = append(pushArgs, "--force")
|
||||||
|
}
|
||||||
|
pushCmd := exec.Command("git", pushArgs...)
|
||||||
|
pushCmd.Stderr = os.Stderr
|
||||||
|
if err := pushCmd.Run(); err != nil {
|
||||||
|
log("! failed to push to %s: %v\n", color.YellowString(remoteName), err)
|
||||||
|
} else {
|
||||||
|
log("> pushed to %s\n", color.GreenString(remoteName))
|
||||||
|
pushSuccesses++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pushSuccesses == 0 {
|
||||||
|
return fmt.Errorf("failed to push to any remote")
|
||||||
|
}
|
||||||
|
|
||||||
|
gitUpdateRefs(ctx, "", *state)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pull",
|
||||||
|
Usage: "pull git changes",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "rebase",
|
||||||
|
Usage: "rebase instead of merge",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// sync to fetch latest state and metadata
|
||||||
|
_, state, err := gitSync(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sync: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out which branches to pull
|
||||||
|
localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the commit from state for the remote branch
|
||||||
|
if state.Event.ID == nostr.ZeroID {
|
||||||
|
return fmt.Errorf("no repository state found")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetCommit, ok := state.Branches[remoteBranch]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("branch '%s' not found in repository state", remoteBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the commit exists locally
|
||||||
|
checkCmd := exec.Command("git", "cat-file", "-e", targetCommit)
|
||||||
|
if err := checkCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge or rebase
|
||||||
|
if c.Bool("rebase") {
|
||||||
|
log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit))
|
||||||
|
rebaseCmd := exec.Command("git", "rebase", targetCommit)
|
||||||
|
rebaseCmd.Stderr = os.Stderr
|
||||||
|
rebaseCmd.Stdout = os.Stdout
|
||||||
|
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)
|
||||||
|
mergeCmd.Stderr = os.Stderr
|
||||||
|
mergeCmd.Stdout = os.Stdout
|
||||||
|
if err := mergeCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("merge failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("pull complete\n")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "fetch",
|
||||||
|
Usage: "fetch git data",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
_, _, err := gitSync(ctx, nil)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptForStringList(
|
func promptForStringList(
|
||||||
@@ -440,351 +688,52 @@ func promptForConfig(config *Nip34Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var gitClone = &cli.Command{
|
func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) {
|
||||||
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.`,
|
|
||||||
ArgsUsage: "<repository> [directory]",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
args := c.Args()
|
|
||||||
if args.Len() == 0 {
|
|
||||||
return fmt.Errorf("missing repository address")
|
|
||||||
}
|
|
||||||
|
|
||||||
owner, identifier, relayHints, err := parseRepositoryAddress(ctx, args.Get(0))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch repository metadata and state
|
|
||||||
repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine target directory
|
|
||||||
targetDir := ""
|
|
||||||
if args.Len() >= 2 {
|
|
||||||
targetDir = args.Get(1)
|
|
||||||
} else {
|
|
||||||
targetDir = repo.ID
|
|
||||||
}
|
|
||||||
if targetDir == "" {
|
|
||||||
targetDir = repo.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// if targetDir exists and is non-empty, bail
|
|
||||||
if fi, err := os.Stat(targetDir); err == nil && fi.IsDir() {
|
|
||||||
entries, err := os.ReadDir(targetDir)
|
|
||||||
if err == nil && len(entries) > 0 {
|
|
||||||
return fmt.Errorf("target directory '%s' already exists and is not empty", targetDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create directory
|
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory '%s': %w", targetDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize git inside the directory
|
|
||||||
initCmd := exec.Command("git", "init")
|
|
||||||
initCmd.Dir = targetDir
|
|
||||||
if err := initCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize git repository: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write nip34.json inside cloned directory
|
|
||||||
localConfig := Nip34Config{
|
|
||||||
Identifier: repo.ID,
|
|
||||||
Name: repo.Name,
|
|
||||||
Description: repo.Description,
|
|
||||||
Web: repo.Web,
|
|
||||||
Owner: nip19.EncodeNpub(repo.Event.PubKey),
|
|
||||||
GraspServers: make([]string, 0, len(repo.Relays)),
|
|
||||||
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
|
|
||||||
Maintainers: make([]string, 0, len(repo.Maintainers)),
|
|
||||||
}
|
|
||||||
for _, r := range repo.Relays {
|
|
||||||
localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r))
|
|
||||||
}
|
|
||||||
for _, m := range repo.Maintainers {
|
|
||||||
localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := localConfig.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("invalid config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write nip34.json
|
|
||||||
if err := writeNip34ConfigFile(targetDir, localConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// add nip34.json to .git/info/exclude in cloned repo
|
|
||||||
excludeNip34ConfigFile(targetDir)
|
|
||||||
|
|
||||||
// setup git remotes
|
|
||||||
gitSetupRemotes(ctx, targetDir, repo)
|
|
||||||
|
|
||||||
// fetch from each grasp remote
|
|
||||||
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 headCommit, ok := state.Branches[state.HEAD]; ok {
|
|
||||||
// check if we have that commit
|
|
||||||
checkCmd := exec.Command("git", "cat-file", "-e", headCommit)
|
|
||||||
checkCmd.Dir = targetDir
|
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
// commit exists, reset to it
|
|
||||||
log("resetting to commit %s...\n", color.CyanString(headCommit))
|
|
||||||
resetCmd := exec.Command("git", "reset", "--hard", headCommit)
|
|
||||||
resetCmd.Dir = targetDir
|
|
||||||
resetCmd.Stderr = os.Stderr
|
|
||||||
if err := resetCmd.Run(); err != nil {
|
|
||||||
log("! failed to reset: %v\n", color.YellowString("%v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update refs from state
|
|
||||||
if state.Event.ID != nostr.ZeroID {
|
|
||||||
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{
|
|
||||||
Name: "force",
|
|
||||||
Aliases: []string{"f"},
|
|
||||||
Usage: "force push to git remotes",
|
|
||||||
}),
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
// setup signer
|
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to gather keyer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// log publishing as npub
|
|
||||||
currentPk, _ := kr.GetPublicKey(ctx)
|
|
||||||
currentNpub := nip19.EncodeNpub(currentPk)
|
|
||||||
log("publishing as %s\n", color.CyanString(currentNpub))
|
|
||||||
|
|
||||||
// sync to ensure everything is up to date
|
|
||||||
repo, state, err := syncRepository(ctx, kr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to sync: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// figure out which branches to push
|
|
||||||
localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if signer matches owner or is in maintainers
|
|
||||||
if currentPk != repo.Event.PubKey && !slices.Contains(repo.Maintainers, currentPk) {
|
|
||||||
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 {
|
|
||||||
return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err)
|
|
||||||
}
|
|
||||||
currentCommit := strings.TrimSpace(string(res))
|
|
||||||
|
|
||||||
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{
|
|
||||||
ID: repo.ID,
|
|
||||||
Branches: make(map[string]string),
|
|
||||||
Tags: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the branch
|
|
||||||
if !c.Bool("force") {
|
|
||||||
if prevCommit, exists := state.Branches[remoteBranch]; exists {
|
|
||||||
// check if prevCommit is an ancestor of currentCommit (fast-forward check)
|
|
||||||
cmd := exec.Command("git", "merge-base", "--is-ancestor", prevCommit, currentCommit)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("non-fast-forward push not allowed, use --force to override")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.Branches[remoteBranch] = currentCommit
|
|
||||||
log("- setting branch %s to commit %s\n", color.CyanString(remoteBranch), color.CyanString(currentCommit))
|
|
||||||
|
|
||||||
// set the HEAD to the local branch if none is set
|
|
||||||
if state.HEAD == "" {
|
|
||||||
state.HEAD = remoteBranch
|
|
||||||
log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and sign the new state event
|
|
||||||
newStateEvent := state.ToEvent()
|
|
||||||
err = kr.SignEvent(ctx, &newStateEvent)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error signing state event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log("- publishing updated repository state to " + color.CyanString("%v", repo.Relays) + "\n")
|
|
||||||
for res := range sys.Pool.PublishMany(ctx, repo.Relays, newStateEvent) {
|
|
||||||
if res.Error != nil {
|
|
||||||
log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
|
||||||
} else {
|
|
||||||
log("> published to %s\n", color.GreenString(res.RelayURL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// push to each grasp remote
|
|
||||||
pushSuccesses := 0
|
|
||||||
for _, relay := range repo.Relays {
|
|
||||||
relayURL := nostr.NormalizeURL(relay)
|
|
||||||
remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://")
|
|
||||||
remoteName = strings.TrimPrefix(remoteName, "ws://")
|
|
||||||
|
|
||||||
log("pushing to %s...\n", color.CyanString(remoteName))
|
|
||||||
pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)}
|
|
||||||
if c.Bool("force") {
|
|
||||||
pushArgs = append(pushArgs, "--force")
|
|
||||||
}
|
|
||||||
pushCmd := exec.Command("git", pushArgs...)
|
|
||||||
pushCmd.Stderr = os.Stderr
|
|
||||||
if err := pushCmd.Run(); err != nil {
|
|
||||||
log("! failed to push to %s: %v\n", color.YellowString(remoteName), err)
|
|
||||||
} else {
|
|
||||||
log("> pushed to %s\n", color.GreenString(remoteName))
|
|
||||||
pushSuccesses++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pushSuccesses == 0 {
|
|
||||||
return fmt.Errorf("failed to push to any remote")
|
|
||||||
}
|
|
||||||
|
|
||||||
gitUpdateRefs(ctx, "", state)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var gitPull = &cli.Command{
|
|
||||||
Name: "pull",
|
|
||||||
Usage: "pull git changes",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "rebase",
|
|
||||||
Usage: "rebase instead of merge",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
// sync to fetch latest state and metadata
|
|
||||||
_, state, err := syncRepository(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to sync: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// figure out which branches to pull
|
|
||||||
localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the commit from state for the remote branch
|
|
||||||
if state.Event.ID == nostr.ZeroID {
|
|
||||||
return fmt.Errorf("no repository state found")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetCommit, ok := state.Branches[remoteBranch]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("branch '%s' not found in repository state", remoteBranch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the commit exists locally
|
|
||||||
checkCmd := exec.Command("git", "cat-file", "-e", targetCommit)
|
|
||||||
if err := checkCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge or rebase
|
|
||||||
if c.Bool("rebase") {
|
|
||||||
log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit))
|
|
||||||
rebaseCmd := exec.Command("git", "rebase", targetCommit)
|
|
||||||
rebaseCmd.Stderr = os.Stderr
|
|
||||||
rebaseCmd.Stdout = os.Stdout
|
|
||||||
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)
|
|
||||||
mergeCmd.Stderr = os.Stderr
|
|
||||||
mergeCmd.Stdout = os.Stdout
|
|
||||||
if err := mergeCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("merge 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)
|
|
||||||
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 syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository, nip34.RepositoryState, error) {
|
|
||||||
// read current nip34.json
|
// read current nip34.json
|
||||||
localConfig, err := readNip34ConfigFile("")
|
localConfig, err := readNip34ConfigFile("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nip34.Repository{}, nip34.RepositoryState{}, err
|
return nip34.Repository{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse owner
|
// parse owner
|
||||||
owner, err := parsePubKey(localConfig.Owner)
|
owner, err := parsePubKey(localConfig.Owner)
|
||||||
if err != nil {
|
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
|
// fetch repository announcement and state from relays
|
||||||
repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logverbose("failed to fetch repository metadata: %v\n", err)
|
log("couldn't fetch repository metadata (%s), will publish now\n", err)
|
||||||
// create a local repository object from config
|
// create a local repository object from config and publish it
|
||||||
repo = localConfig.ToRepository()
|
localRepo := localConfig.ToRepository()
|
||||||
|
|
||||||
|
if signer != nil {
|
||||||
|
signerPk, err := signer.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
} else {
|
||||||
// check if local config differs from remote announcement
|
// check if local config differs from remote announcement
|
||||||
// construct local repo from config for comparison
|
// construct local repo from config for comparison
|
||||||
@@ -853,8 +802,8 @@ func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository,
|
|||||||
fetchFromRemotes(ctx, "", repo)
|
fetchFromRemotes(ctx, "", repo)
|
||||||
|
|
||||||
// update refs from state
|
// update refs from state
|
||||||
if state.Event.ID != nostr.ZeroID {
|
if state != nil {
|
||||||
gitUpdateRefs(ctx, "", state)
|
gitUpdateRefs(ctx, "", *state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo, state, nil
|
return repo, state, nil
|
||||||
@@ -986,7 +935,7 @@ func fetchRepositoryAndState(
|
|||||||
pubkey nostr.PubKey,
|
pubkey nostr.PubKey,
|
||||||
identifier string,
|
identifier string,
|
||||||
relayHints []string,
|
relayHints []string,
|
||||||
) (repo nip34.Repository, state nip34.RepositoryState, err error) {
|
) (repo nip34.Repository, state *nip34.RepositoryState, err error) {
|
||||||
// fetch repository announcement (30617)
|
// fetch repository announcement (30617)
|
||||||
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
||||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
@@ -996,7 +945,7 @@ func fetchRepositoryAndState(
|
|||||||
"d": []string{identifier},
|
"d": []string{identifier},
|
||||||
},
|
},
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) {
|
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
|
||||||
if ie.Event.CreatedAt > repo.CreatedAt {
|
if ie.Event.CreatedAt > repo.CreatedAt {
|
||||||
repo = nip34.ParseRepository(ie.Event)
|
repo = nip34.ParseRepository(ie.Event)
|
||||||
}
|
}
|
||||||
@@ -1006,7 +955,6 @@ func fetchRepositoryAndState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository state (30618)
|
// fetch repository state (30618)
|
||||||
var stateFound bool
|
|
||||||
var stateErr error
|
var stateErr error
|
||||||
for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{
|
for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{
|
||||||
Kinds: []nostr.Kind{30618},
|
Kinds: []nostr.Kind{30618},
|
||||||
@@ -1015,10 +963,10 @@ func fetchRepositoryAndState(
|
|||||||
"d": []string{identifier},
|
"d": []string{identifier},
|
||||||
},
|
},
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) {
|
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
|
||||||
if ie.Event.CreatedAt > state.CreatedAt {
|
if state == nil || ie.Event.CreatedAt > state.CreatedAt {
|
||||||
state = nip34.ParseRepositoryState(ie.Event)
|
state_ := nip34.ParseRepositoryState(ie.Event)
|
||||||
stateFound = true
|
state = &state_
|
||||||
|
|
||||||
if state.HEAD == "" {
|
if state.HEAD == "" {
|
||||||
stateErr = fmt.Errorf("state is missing HEAD")
|
stateErr = fmt.Errorf("state is missing HEAD")
|
||||||
@@ -1032,9 +980,6 @@ func fetchRepositoryAndState(
|
|||||||
stateErr = nil
|
stateErr = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !stateFound {
|
|
||||||
return repo, state, fmt.Errorf("no repository state (kind 30618) found")
|
|
||||||
}
|
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return repo, state, stateErr
|
return repo, state, stateErr
|
||||||
}
|
}
|
||||||
@@ -1313,3 +1258,65 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] }
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user