internal/control/commitfile.go

4744c629ed4f0158bdea7d0f1d207fdccb1f7f99
gitbay/internal/control/commitfile.go history · blame · raw

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