Compare commits

...

3 Commits

Author SHA1 Message Date
fiatjaf
8df130a822 git: handle "pull" modes correctly and stop deleting and recreating remotes all the time. 2025-11-25 14:51:24 -03:00
fiatjaf
e04861fcee git: allow gitSync to not fail if the state is broken. 2025-11-25 14:51:24 -03:00
Yasuhiro Matsumoto
73d80203a0 fix error message 2025-11-25 14:35:16 -03:00
2 changed files with 145 additions and 35 deletions

View File

@@ -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)
}

178
git.go
View File

@@ -302,7 +302,7 @@ aside from those, there is also:
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,6 +458,18 @@ aside from those, there is also:
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
@@ -488,8 +500,51 @@ aside from those, there is also:
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
@@ -497,14 +552,52 @@ aside from those, there is also:
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")
@@ -703,7 +796,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
// fetch repository announcement and state from relays
repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
if err != nil {
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()
@@ -735,6 +828,15 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
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()
@@ -840,39 +942,43 @@ 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/") {
delCmd := exec.Command("git", "remote", "remove", remote)
if dir != "" {
delCmd.Dir = dir
}
if err := delCmd.Run(); err != nil {
logverbose("failed to remove remote %s: %v\n", remote, err)
if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) {
delCmd := exec.Command("git", "remote", "remove", remote)
if dir != "" {
delCmd.Dir = dir
}
if err := delCmd.Run(); err != nil {
logverbose("failed to remove remote %s: %v\n", remote, err)
}
}
}
}
// 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://")
// construct the git URL
gitURL := fmt.Sprintf("http%s/%s/%s.git",
relayURL[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
if !slices.Contains(remotes, remote) {
// construct the git URL
gitURL := fmt.Sprintf("http%s/%s/%s.git",
relay[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
addCmd := exec.Command("git", "remote", "add", remoteName, gitURL)
if dir != "" {
addCmd.Dir = dir
}
if out, err := addCmd.Output(); err != nil {
var stderr string
if exiterr, ok := err.(*exec.ExitError); ok {
stderr = string(exiterr.Stderr)
addCmd := exec.Command("git", "remote", "add", remote, gitURL)
if dir != "" {
addCmd.Dir = dir
}
if out, err := addCmd.Output(); err != nil {
var stderr string
if exiterr, ok := err.(*exec.ExitError); ok {
stderr = string(exiterr.Stderr)
}
logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out))
}
logverbose("failed to add remote %s: %s %s\n", remoteName, stderr, string(out))
}
}
}
@@ -955,7 +1061,7 @@ func fetchRepositoryAndState(
}
// fetch repository state (30618)
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},
@@ -966,18 +1072,18 @@ func fetchRepositoryAndState(
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
if state == nil || ie.Event.CreatedAt > state.CreatedAt {
state_ := nip34.ParseRepositoryState(ie.Event)
state = &state_
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 stateErr != nil {
@@ -987,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