Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 59m ago 4b8c9e1d
history blame raw 3.0 KiB
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "os/exec"
8 "strings"
9
10 "rodent.software/vole/client"
11)
12
13// releaseCLI creates the one thing a release is: an annotated tag.
14//
15// There is no release record to write and no upload to perform — the web derives
16// the release page and the source tarball from the tag, so a clone that has the
17// tag has the release. This command exists because `git tag -a` has three ways to
18// get the annotation wrong (lightweight tag, empty message, wrong tagger), and
19// saying so once is cheaper than teaching each user separately.
20
21func releaseUsage() string {
22 return `usage: vole release create <tag> (-m <text> | -F <file|->)
23 or: vole release attach <tag> <file>... [--remote <name>]
24
25A release is an annotated tag: its message is the release notes and its tree is
26the source tarball, so both travel with any clone. This writes the tag in the
27current repository. Push it with:
28
29 git push --follow-tags`
30}
31
32func cmdRelease(args []string) error {
33 if len(args) == 0 {
34 fmt.Fprintln(os.Stderr, releaseUsage())
35 return usageErr{msg: "no release subcommand given"}
36 }
37 switch args[0] {
38 case "create":
39 return cmdReleaseCreate(args[1:])
40 case "attach":
41 return cmdReleaseAttach(args[1:])
42 case "-h", "--help", "help":
43 fmt.Println(releaseUsage())
44 return nil
45 }
46 fmt.Fprintln(os.Stderr, releaseUsage())
47 return usagef("unknown release subcommand %q", args[0])
48}
49
50func cmdReleaseCreate(args []string) error {
51 f, err := parseFlags(args,
52 flagSpec{long: "message", short: "m"},
53 flagSpec{long: "file", short: "F"},
54 flagSpec{long: "help", short: "h", boolean: true},
55 )
56 if err != nil {
57 return err
58 }
59 if f.help {
60 fmt.Println(releaseUsage())
61 return nil
62 }
63 if len(f.rest) != 1 {
64 return usagef("usage: vole release create <tag> (-m <text> | -F <file|->)")
65 }
66 name := f.rest[0]
67 if strings.HasPrefix(name, "-") {
68 return usagef("invalid tag name %q", name)
69 }
70
71 notes, err := readBody(f)
72 if err != nil {
73 return err
74 }
75 if strings.TrimSpace(notes) == "" {
76 return usagef("a release needs notes: pass -m <text> or -F <file>.\n"+
77 "An annotated tag with an empty message is a bare tag; use git tag %s if that is what you want", name)
78 }
79
80 dir, err := client.CurrentGitDir()
81 if err != nil {
82 return err
83 }
84
85 // git refuses the tag too, but its message ("already exists") does not say
86 // that the existing tag is what the web is already serving.
87 if exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet",
88 "refs/tags/"+name).Run() == nil {
89 return fmt.Errorf("tag %s already exists; a release is immutable once pushed. "+
90 "Tag the new commit with a new name instead", name)
91 }
92
93 cmd := exec.Command("git", "-C", dir, "tag", "-a", name, "-F", "-")
94 cmd.Stdin = bytes.NewReader([]byte(notes))
95 var stderr bytes.Buffer
96 cmd.Stderr = &stderr
97 if err := cmd.Run(); err != nil {
98 return fmt.Errorf("git tag: %w: %s", err, strings.TrimSpace(stderr.String()))
99 }
100
101 fmt.Printf("tagged %s (annotated)\n", name)
102 fmt.Printf("push it: git push --follow-tags\n")
103 return nil
104}
105