git betterments with remote and branch determination, force-push and fast-forward check.

This commit is contained in:
fiatjaf
2025-11-17 20:18:51 -03:00
parent bbe1661096
commit 5d7240b112
3 changed files with 159 additions and 105 deletions

257
git.go
View File

@@ -212,25 +212,8 @@ var gitInit = &cli.Command{
ownerNpub := nip19.EncodeNpub(pk)
// check existing git remotes
cmd = exec.Command("git", "remote", "-v")
output, err := cmd.Output()
nostrRemote, _, _, err := getGitNostrRemote(c)
if err != nil {
return fmt.Errorf("failed to get git remotes: %w", err)
}
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
var nostrRemote string
for _, remote := range remotes {
if strings.Contains(remote, "nostr://") {
parts := strings.Fields(remote)
if len(parts) >= 2 {
nostrRemote = parts[1]
break
}
}
}
if nostrRemote == "" {
remoteURL := fmt.Sprintf("nostr://%s/%s/%s", ownerNpub, config.GraspServers[0], config.Identifier)
cmd = exec.Command("git", "remote", "add", "origin", remoteURL)
if err := cmd.Run(); err != nil {
@@ -400,37 +383,14 @@ func promptForConfig(config *Nip34Config) error {
return nil
}
func gitSanityCheck(localConfig Nip34Config, nostrRemote string) (nostr.PubKey, error) {
urlParts := strings.TrimPrefix(nostrRemote, "nostr://")
parts := strings.Split(urlParts, "/")
if len(parts) != 3 {
return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr://<npub>/<relay_hostname>/<identifier>, got: %s", nostrRemote)
}
remoteNpub := parts[0]
remoteHostname := parts[1]
remoteIdentifier := parts[2]
ownerPk, err := parsePubKey(localConfig.Owner)
if err != nil {
return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err)
}
if nip19.EncodeNpub(ownerPk) != remoteNpub {
return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub")
}
if remoteIdentifier != localConfig.Identifier {
return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier)
}
if !slices.Contains(localConfig.GraspServers, remoteHostname) {
return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers)
}
return ownerPk, nil
}
var gitPush = &cli.Command{
Name: "push",
Usage: "push git changes",
Flags: defaultKeyFlags,
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)
@@ -455,26 +415,9 @@ var gitPush = &cli.Command{
}
// get git remotes
cmd := exec.Command("git", "remote", "-v")
output, err := cmd.Output()
nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c)
if err != nil {
return fmt.Errorf("failed to get git remotes: %w", err)
}
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
var nostrRemote string
for _, remote := range remotes {
if strings.Contains(remote, "nostr://") {
parts := strings.Fields(remote)
if len(parts) >= 2 {
nostrRemote = parts[1]
break
}
}
}
if nostrRemote == "" {
return fmt.Errorf("no nostr:// remote found")
return err
}
// parse the URL: nostr://<npub>/<relay_hostname>/<identifier>
@@ -521,20 +464,14 @@ var gitPush = &cli.Command{
log("found state event: %s\n", state.Event.ID)
}
// get current branch and commit
res, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
// get commit for the local branch
res, err := exec.Command("git", "rev-parse", localBranch).Output()
if err != nil {
return fmt.Errorf("failed to get current branch: %w", err)
}
currentBranch := strings.TrimSpace(string(res))
res, err = exec.Command("git", "rev-parse", "HEAD").Output()
if err != nil {
return fmt.Errorf("failed to get current commit: %w", err)
return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err)
}
currentCommit := strings.TrimSpace(string(res))
log("current branch: %s, commit: %s\n", currentBranch, currentCommit)
log("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 {
@@ -546,13 +483,22 @@ var gitPush = &cli.Command{
}
// update the branch
state.Branches[currentBranch] = currentCommit
log("> setting branch %s to commit %s\n", currentBranch, currentCommit)
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", remoteBranch, currentCommit)
// set the HEAD to the current branch if none is set
// set the HEAD to the local branch if none is set
if state.HEAD == "" {
state.HEAD = currentBranch
log("> setting HEAD to branch %s\n", currentBranch)
state.HEAD = remoteBranch
log("> setting HEAD to branch %s\n", remoteBranch)
}
// create and sign the new state event
@@ -573,13 +519,21 @@ var gitPush = &cli.Command{
// push to git clone URLs
for _, cloneURL := range repo.Clone {
log("> pushing to: %s\n", cloneURL)
cmd := exec.Command("git", "push", cloneURL, fmt.Sprintf("refs/heads/%s:refs/heads/%s", currentBranch, currentBranch))
log("> pushing to: %s\n", color.CyanString(cloneURL))
args := []string{"push"}
if c.Bool("force") {
args = append(args, "--force")
}
args = append(args,
cloneURL,
fmt.Sprintf("refs/heads/%s:refs/heads/%s", localBranch, remoteBranch),
)
cmd := exec.Command("git", args...)
output, err := cmd.CombinedOutput()
if err != nil {
log("(!) failed to push to %s: %v\n%s\n", cloneURL, err, string(output))
log("> failed to push to %s: %v\n%s\n", color.RedString(cloneURL), err, string(output))
} else {
log("> successfully pushed to %s\n", cloneURL)
log("> successfully pushed to %s\n", color.GreenString(cloneURL))
}
}
@@ -626,26 +580,9 @@ var gitAnnounce = &cli.Command{
}
// get git remotes
cmd = exec.Command("git", "remote", "-v")
output, err := cmd.Output()
nostrRemote, _, _, err := getGitNostrRemote(c)
if err != nil {
return fmt.Errorf("failed to get git remotes: %w", err)
}
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
var nostrRemote string
for _, remote := range remotes {
if strings.Contains(remote, "nostr://") {
parts := strings.Fields(remote)
if len(parts) >= 2 {
nostrRemote = parts[1]
break
}
}
}
if nostrRemote == "" {
return fmt.Errorf("no nostr:// remote found")
return err
}
ownerPk, err := gitSanityCheck(localConfig, nostrRemote)
@@ -734,3 +671,117 @@ var gitAnnounce = &cli.Command{
return nil
},
}
func getGitNostrRemote(c *cli.Command) (
remoteURL string,
localBranch string,
remoteBranch string,
err error,
) {
// remote
var remoteName string
var cmd *exec.Cmd
args := c.Args()
if args.Len() > 0 {
remoteName = args.Get(0)
} else {
// get current branch
cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", "", "", fmt.Errorf("failed to get current branch: %w", err)
}
branch := strings.TrimSpace(string(output))
// get remote for branch
cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch))
output, err = cmd.Output()
if err != nil {
remoteName = "origin"
} else {
remoteName = strings.TrimSpace(string(output))
}
}
// get the URL
cmd = exec.Command("git", "remote", "get-url", remoteName)
output, err := cmd.Output()
if err != nil {
return "", "", "", fmt.Errorf("remote '%s' does not exist", remoteName)
}
remoteURL = strings.TrimSpace(string(output))
if !strings.Contains(remoteURL, "nostr://") {
return "", "", "", fmt.Errorf("remote '%s' is not a nostr remote: %s", remoteName, remoteURL)
}
// branch (local and remote)
if args.Len() > 1 {
branchSpec := args.Get(1)
if strings.Contains(branchSpec, ":") {
parts := strings.Split(branchSpec, ":")
if len(parts) == 2 {
localBranch = parts[0]
remoteBranch = parts[1]
} else {
return "", "", "", fmt.Errorf("invalid branch spec: %s", branchSpec)
}
} else {
localBranch = branchSpec
}
} else {
// get current branch
cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", "", "", fmt.Errorf("failed to get current branch: %w", err)
}
localBranch = strings.TrimSpace(string(output))
}
// get the upstream branch from git config
cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch))
output, err = cmd.Output()
if err == nil {
// parse refs/heads/<branch-name> to get just the branch name
mergeRef := strings.TrimSpace(string(output))
if strings.HasPrefix(mergeRef, "refs/heads/") {
remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/")
} else {
// fallback if it's not in expected format
remoteBranch = localBranch
}
} else {
// no upstream configured, assume same branch name
remoteBranch = localBranch
}
return remoteURL, localBranch, remoteBranch, nil
}
func gitSanityCheck(
localConfig Nip34Config,
nostrRemote string,
) (nostr.PubKey, error) {
urlParts := strings.TrimPrefix(nostrRemote, "nostr://")
parts := strings.Split(urlParts, "/")
if len(parts) != 3 {
return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr://<npub>/<relay_hostname>/<identifier>, got: %s", nostrRemote)
}
remoteNpub := parts[0]
remoteHostname := parts[1]
remoteIdentifier := parts[2]
ownerPk, err := parsePubKey(localConfig.Owner)
if err != nil {
return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err)
}
if nip19.EncodeNpub(ownerPk) != remoteNpub {
return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub")
}
if remoteIdentifier != localConfig.Identifier {
return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier)
}
if !slices.Contains(localConfig.GraspServers, remoteHostname) {
return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers)
}
return ownerPk, nil
}

1
go.mod
View File

@@ -30,6 +30,7 @@ require (
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
github.com/bluekeyes/go-gitdiff v0.7.1 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect

6
go.sum
View File

@@ -13,6 +13,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
@@ -92,8 +94,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=