all repos

mugit @ b6e1830

🐮 git server that your cow will love

mugit/internal/git/gitservice/gitservice.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
some refactoring, 4 months ago
1
package gitservice
2
3
import (
4
	"bytes"
5
	"fmt"
6
	"io"
7
	"os/exec"
8
	"sync"
9
	"syscall"
10
)
11
12
// InfoRefs executes git-upload-pack --advertise-refs for smart-HTTP discovery.
13
func InfoRefs(dir string, out io.Writer) error {
14
	cmd := exec.Command("git",
15
		"upload-pack",
16
		"--stateless-rpc",
17
		"--advertise-refs",
18
		".")
19
	cmd.Dir = dir
20
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
21
22
	stdout, err := cmd.StdoutPipe()
23
	if err != nil {
24
		return fmt.Errorf("stdout pipe: %w", err)
25
	}
26
	cmd.Stderr = cmd.Stdout
27
28
	if err := cmd.Start(); err != nil {
29
		return fmt.Errorf("start git-upload-pack: %w", err)
30
	}
31
32
	if err := PackLine(out, "# service=git-upload-pack\n"); err != nil {
33
		return fmt.Errorf("write pack line: %w", err)
34
	}
35
	if err := PackFlush(out); err != nil {
36
		return fmt.Errorf("flush pack: %w", err)
37
	}
38
39
	var buf bytes.Buffer
40
	if _, err := io.Copy(&buf, stdout); err != nil {
41
		return fmt.Errorf("copy stdout to buffer: %w", err)
42
	}
43
44
	if err := cmd.Wait(); err != nil {
45
		return fmt.Errorf("git-upload-pack: %w", err)
46
	}
47
48
	if _, err := io.Copy(out, &buf); err != nil {
49
		return fmt.Errorf("copy buffer to output: %w", err)
50
	}
51
52
	return nil
53
}
54
55
// UploadPack executes git-upload-pack for smart-HTTP git fetch/clone.
56
// StatelessRPC should be true in case it's used over http, and false for ssh.
57
func UploadPack(dir string, statelessRPC bool, in io.Reader, out io.Writer) error {
58
	return gitCmd("upload-pack", config{
59
		Dir:          dir,
60
		StatelessRPC: statelessRPC,
61
		AllowFilter:  true,
62
		Stdin:        in,
63
		Stdout:       out,
64
	})
65
}
66
67
func ReceivePack(dir string, in io.Reader, out, errout io.Writer) error {
68
	return gitCmd("receive-pack", config{
69
		Dir:    dir,
70
		Stdin:  in,
71
		Stdout: out,
72
		Stderr: errout,
73
	})
74
}
75
76
type config struct {
77
	Dir          string
78
	StatelessRPC bool
79
	AllowFilter  bool
80
	ExtraArgs    []string
81
	Stdin        io.Reader
82
	Stdout       io.Writer
83
	Stderr       io.Writer
84
}
85
86
func gitCmd(service string, c config) error {
87
	args := []string{}
88
	if c.AllowFilter {
89
		args = append(args, "-c", "uploadpack.allowFilter=true")
90
	}
91
92
	args = append(args, service)
93
	if c.StatelessRPC {
94
		args = append(args, "--stateless-rpc")
95
	}
96
97
	args = append(args, c.ExtraArgs...)
98
	args = append(args, ".")
99
100
	cmd := exec.Command("git", args...)
101
	cmd.Dir = c.Dir
102
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
103
104
	var (
105
		err   error
106
		stdin io.WriteCloser
107
	)
108
109
	if c.Stdin != nil {
110
		stdin, err = cmd.StdinPipe()
111
		if err != nil {
112
			return err
113
		}
114
	}
115
116
	stdout, err := cmd.StdoutPipe()
117
	if err != nil {
118
		return err
119
	}
120
	if c.Stderr != nil {
121
		cmd.Stderr = c.Stderr
122
	} else {
123
		cmd.Stderr = cmd.Stdout
124
	}
125
126
	if err := cmd.Start(); err != nil {
127
		return fmt.Errorf("start %s: %w", service, err)
128
	}
129
130
	if c.Stdin != nil {
131
		// Don't add to WaitGroup - stdin closes when client closes,
132
		// shouldn't block waiting for output to finish
133
		go func() {
134
			defer stdin.Close()
135
			io.Copy(stdin, c.Stdin)
136
		}()
137
	}
138
139
	var wg sync.WaitGroup
140
	var stdoutErr error
141
142
	wg.Go(func() {
143
		_, stdoutErr = io.Copy(c.Stdout, stdout)
144
	})
145
146
	wg.Wait()
147
148
	if stdoutErr != nil {
149
		return fmt.Errorf("copy stdout: %w", stdoutErr)
150
	}
151
152
	if err := cmd.Wait(); err != nil {
153
		return fmt.Errorf("%s: %w", service, err)
154
	}
155
	return nil
156
}
157
158
// PackLine writes a pkt-line formatted string.
159
func PackLine(w io.Writer, s string) error {
160
	_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
161
	return err
162
}
163
164
// PackFlush writes a flush packet.
165
func PackFlush(w io.Writer) error {
166
	_, err := fmt.Fprint(w, "0000")
167
	return err
168
}
169
170
// PackSideband writes a message to sideband channel (displays as "remote: <msg>" in git client).
171
// Channel 2 = progress/info, Channel 3 = error.
172
func PackSideband(w io.Writer, channel byte, msg string) error {
173
	return PackLine(w, string(channel)+msg)
174
}
175
176
// PackError writes an ERR packet for protocol-level errors.
177
// Git displays this as: fatal: remote error: <msg>
178
func PackError(w io.Writer, msg string) error {
179
	return PackLine(w, "ERR "+msg)
180
}