internal/control/commitfile.go

aa6d5bfab86d3163d485be9d89b313c2b0546009
gitbay/internal/control/commitfile.go history · blame · raw

123 lines · 3700 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	var rest []string
 39	var ref, message, file string
 40	for i := 0; i < len(args); i++ {
 41		switch args[i] {
 42		case "--ref", "--message", "--file":
 43			if i+1 >= len(args) {
 44				return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
 45			}
 46			switch args[i] {
 47			case "--ref":
 48				ref = args[i+1]
 49			case "--message":
 50				message = args[i+1]
 51			case "--file":
 52				file = args[i+1]
 53			}
 54			i++
 55		default:
 56			if strings.HasPrefix(args[i], "--") {
 57				return c.fail(protocol.ExitUsage, "unknown flag %q\nusage: %s", args[i], usage)
 58			}
 59			rest = append(rest, args[i])
 60		}
 61	}
 62	if len(rest) != 2 || ref == "" {
 63		return c.fail(protocol.ExitUsage, "usage: %s", usage)
 64	}
 65	repo, code := resolveRepo(c, rest[0], policy.CanWrite)
 66	if code >= 0 {
 67		return code
 68	}
 69	if code := refuseArchived(c, repo); code >= 0 {
 70		return code
 71	}
 72	filePath, ok := cleanRepoPath(rest[1])
 73	if !ok || filePath == "" {
 74		return c.fail(protocol.ExitUsage, "path must stay inside the repository")
 75	}
 76	// The server authors this commit, so it cannot sign it.
 77	if repo.Settings.RequireSignedCommits {
 78		return c.fail(protocol.ExitDenied,
 79			"%s requires signed commits; this writes an unsigned one — push a signed commit instead",
 80			repo.Path())
 81	}
 82	// A commit carries an identity, and an unverified address is not one.
 83	email, err := c.Store.PrimaryVerifiedEmail(c.User.ID)
 84	if err != nil {
 85		return c.fail(protocol.ExitFailure, "%v", err)
 86	}
 87	if email == "" {
 88		return c.fail(protocol.ExitDenied,
 89			"commits carry your identity: your account needs a verified primary email")
 90	}
 91
 92	var content []byte
 93	if file != "" {
 94		if file != "-" {
 95			return c.fail(protocol.ExitUsage, "--file only supports - (stdin)")
 96		}
 97		content, err = io.ReadAll(io.LimitReader(c.Stdin, maxCommitFileBytes))
 98		if err != nil {
 99			return c.fail(protocol.ExitFailure, "reading content: %v", err)
100		}
101	}
102	if message = strings.TrimSpace(message); message == "" {
103		message = "edit " + filePath
104	}
105
106	dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
107	sha, err := gitutil.CommitFileChange(dir, ref, filePath, content,
108		c.User.Username, email, message)
109	if err != nil {
110		return c.fail(protocol.ExitFailure, "%v", err)
111	}
112	c.Store.MarkMirrorsDirty(repo.ID, "push")
113
114	d := struct {
115		Path string `json:"path"`
116		Ref  string `json:"ref"`
117		File string `json:"file"`
118		SHA  string `json:"sha"`
119	}{repo.Path(), ref, filePath, sha}
120	return c.emit(d, func(w io.Writer) {
121		fmt.Fprintf(w, "committed %s on %s: %.10s\n", filePath, ref, sha)
122	})
123}