internal/control/commitfile.go
105 lines · 3402 bytes
1package control
2
3import (
4 "fmt"
5 "io"
6 "strings"
7
8 "gitbay.org/gitbay/internal/gitutil"
9 "gitbay.org/gitbay/internal/policy"
10 "gitbay.org/gitbay/internal/protocol"
11)
12
13func init() {
14 register(Command{
15 Path: []string{"repo", "commit-file"},
16 Summary: "write a file and commit it",
17 Usage: "repo commit-file <owner/name> <path> " +
18 "--ref <branch> [--message <m>] [--file -]",
19 ReadsStdin: true,
20 Run: runCommitFile,
21 })
22}
23
24// maxCommitFileBytes bounds one edit. Large content belongs in a push,
25// not a single-file commit over the control plane.
26const maxCommitFileBytes = 1 << 20
27
28// runCommitFile commits one file's contents to a branch. It exists so the
29// capability is reachable from every surface: the web's editor dispatches
30// this rather than calling git itself, which is what kept editing off the
31// CLI and the API.
32//
33// Commits made here are unsigned, because the server is authoring them.
34// A repository that requires verified signatures therefore refuses the
35// command rather than writing a commit its own policy would reject.
36func runCommitFile(c *Ctx, args []string) int {
37 const usage = "repo commit-file <owner/name> <path> --ref <branch> [--message <m>] [--file -]"
38 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--message", "--file"}, MaxPos: -1, Usage: usage})
39 if err != nil {
40 return c.fail(protocol.ExitUsage, "%v", err)
41 }
42 rest := f.Pos
43 ref, message, file := f.Value("--ref"), f.Value("--message"), f.Value("--file")
44 if len(rest) != 2 || ref == "" {
45 return c.fail(protocol.ExitUsage, "usage: %s", usage)
46 }
47 repo, code := resolveRepo(c, rest[0], policy.CanWrite)
48 if code >= 0 {
49 return code
50 }
51 if code := refuseArchived(c, repo); code >= 0 {
52 return code
53 }
54 filePath, ok := cleanRepoPath(rest[1])
55 if !ok || filePath == "" {
56 return c.fail(protocol.ExitUsage, "path must stay inside the repository")
57 }
58 // The server authors this commit, so it cannot sign it.
59 if repo.Settings.RequireSignedCommits {
60 return c.fail(protocol.ExitDenied,
61 "%s requires signed commits; this writes an unsigned one — push a signed commit instead",
62 repo.Path())
63 }
64 // A commit carries an identity, and an unverified address is not one.
65 email, err := c.Store.PrimaryVerifiedEmail(c.User.ID)
66 if err != nil {
67 return c.fail(protocol.ExitFailure, "%v", err)
68 }
69 if email == "" {
70 return c.fail(protocol.ExitDenied,
71 "commits carry your identity: your account needs a verified primary email")
72 }
73
74 var content []byte
75 if file != "" {
76 if file != "-" {
77 return c.fail(protocol.ExitUsage, "--file only supports - (stdin)")
78 }
79 content, err = io.ReadAll(io.LimitReader(c.Stdin, maxCommitFileBytes))
80 if err != nil {
81 return c.fail(protocol.ExitFailure, "reading content: %v", err)
82 }
83 }
84 if message = strings.TrimSpace(message); message == "" {
85 message = "edit " + filePath
86 }
87
88 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
89 sha, err := gitutil.CommitFileChange(dir, ref, filePath, content,
90 c.User.Username, email, message)
91 if err != nil {
92 return c.fail(protocol.ExitFailure, "%v", err)
93 }
94 c.Store.MarkMirrorsDirty(repo.ID, "push")
95
96 d := struct {
97 Path string `json:"path"`
98 Ref string `json:"ref"`
99 File string `json:"file"`
100 SHA string `json:"sha"`
101 }{repo.Path(), ref, filePath, sha}
102 return c.emit(d, func(w io.Writer) {
103 fmt.Fprintf(w, "committed %s on %s: %.10s\n", filePath, ref, sha)
104 })
105}