gitlyx / git / git.v
220 lines · 202 sloc · 5.56 KB · f01d3f77ed35a21882e0e1e97310a13b0a9dfd8b
Raw
1module git
2
3import os
4import time
5
6pub const clone_size_limit_exit_code = 125
7pub const clone_size_limit_marker = '[gitly:clone-size-limit]'
8
9pub struct Git {}
10
11pub fn Git.exec(args []string) os.Result {
12 mut git_args := ['git']
13 git_args << args
14 return os.exec(git_args)
15}
16
17pub fn Git.exec_in_dir(dir string, args []string) os.Result {
18 mut git_args := ['-C', dir]
19 git_args << args
20 return Git.exec(git_args)
21}
22
23pub fn Git.exec_in_dir_command(dir string, command string) os.Result {
24 return Git.exec_in_dir(dir, split_command(command))
25}
26
27pub fn Git.exec_shell(command string) os.Result {
28 return os.exec(['/bin/sh', '-c', command])
29}
30
31// Git.exec_in_dir_with_env runs `git -C <dir> <args...>` without going through a
32// shell, with `extra_env` added on top of the inherited environment. Use this
33// instead of building a shell string when any argument or environment value is
34// user-controlled (file paths, branch names, commit messages, author names),
35// since arguments are passed directly to git and never re-parsed by /bin/sh.
36pub fn Git.exec_in_dir_with_env(dir string, args []string, extra_env map[string]string) os.Result {
37 mut full_args := ['-C', dir]
38 full_args << args
39 mut p := os.new_process('git')
40 p.set_args(full_args)
41 mut merged := os.environ()
42 for k, v in extra_env {
43 merged[k] = v
44 }
45 p.set_environment(merged)
46 p.set_redirect_stdio()
47 p.run()
48 output := p.stdout_slurp() + p.stderr_slurp()
49 p.wait()
50 code := p.code
51 p.close()
52 return os.Result{
53 exit_code: code
54 output: output
55 }
56}
57
58pub fn Git.clone(url string, path string) os.Result {
59 println('new clone url="${url}" path="${path}"')
60 return os.exec(['git', 'clone', '--bare', url, path])
61}
62
63// Git.clone_with_progress runs `git clone --bare --progress` and streams
64// every byte of git's progress output (which goes to stderr) into
65// `progress_path` while the clone is running, so a separate process can
66// poll the file and show live progress to the user.
67pub fn Git.clone_with_progress(url string, path string, progress_path string) os.Result {
68 return Git.clone_with_progress_limit(url, path, progress_path, 0)
69}
70
71pub fn Git.clone_with_progress_limit(url string, path string, progress_path string, max_bytes u64) os.Result {
72 println('new clone (progress) url="${url}" path="${path}" progress="${progress_path}"')
73 os.rm(progress_path) or {}
74 mut p := os.new_process('git')
75 p.set_args(['clone', '--bare', '--progress', url, path])
76 p.set_redirect_stdio()
77 p.run()
78 mut log := os.open_append(progress_path) or {
79 eprintln('clone_with_progress: cannot open progress file "${progress_path}": ${err}')
80 // fall back to non-streaming behaviour
81 p.wait()
82 out := p.stdout_slurp() + p.stderr_slurp()
83 code := p.code
84 p.close()
85 return os.Result{
86 exit_code: code
87 output: out
88 }
89 }
90 mut collected := ''
91 mut stopped_for_size := false
92 for p.is_alive() {
93 chunk := p.stderr_read()
94 if chunk.len > 0 {
95 log.write_string(chunk) or {}
96 log.flush()
97 collected += chunk
98 if max_bytes > 0 && !stopped_for_size
99 && clone_progress_received_bytes(collected) >= max_bytes {
100 stopped_for_size = true
101 marker := '\n${clone_size_limit_marker}\n'
102 log.write_string(marker) or {}
103 log.flush()
104 collected += marker
105 p.signal_term()
106 time.sleep(500 * time.millisecond)
107 if p.is_alive() {
108 p.signal_kill()
109 }
110 }
111 }
112 // drain stdout so the pipe buffer never blocks the child
113 _ := p.stdout_read()
114 time.sleep(100 * time.millisecond)
115 }
116 final := p.stderr_slurp()
117 if final.len > 0 {
118 log.write_string(final) or {}
119 log.flush()
120 collected += final
121 }
122 log.close()
123 p.wait()
124 exit_code := if stopped_for_size { clone_size_limit_exit_code } else { p.code }
125 p.close()
126 return os.Result{
127 exit_code: exit_code
128 output: collected
129 }
130}
131
132pub fn clone_progress_received_bytes(progress string) u64 {
133 mut max_bytes := u64(0)
134 for line in progress.replace('\r', '\n').split('\n') {
135 if !line.contains('Receiving objects:') {
136 continue
137 }
138 bytes := parse_clone_progress_size(line) or { continue }
139 if bytes > max_bytes {
140 max_bytes = bytes
141 }
142 }
143 return max_bytes
144}
145
146fn parse_clone_progress_size(line string) ?u64 {
147 comma := line.index('),') or { return none }
148 mut size_part := line[comma + 2..].trim_space()
149 pipe := size_part.index('|') or { size_part.len }
150 size_part = size_part[..pipe].trim_space()
151 parts := size_part.fields()
152 if parts.len < 2 {
153 return none
154 }
155 value := parts[0].f64()
156 multiplier := match parts[1] {
157 'B', 'byte', 'bytes' { 1.0 }
158 'KiB' { 1024.0 }
159 'MiB' { 1024.0 * 1024.0 }
160 'GiB' { 1024.0 * 1024.0 * 1024.0 }
161 else { return none }
162 }
163
164 return u64(value * multiplier)
165}
166
167pub fn Git.fetch_ref(repo_dir string, remote string, refspec string) os.Result {
168 return Git.exec_in_dir(repo_dir, ['fetch', remote, refspec])
169}
170
171pub fn Git.show_file_blob(repo_dir string, branch string, file_path string) !string {
172 result := Git.exec_in_dir(repo_dir, ['--no-pager', 'show', '${branch}:${file_path}'])
173 if result.exit_code != 0 {
174 return error(result.output)
175 }
176 return result.output
177}
178
179fn split_command(command string) []string {
180 mut args := []string{}
181 mut current := []u8{}
182 mut quote := u8(0)
183 mut escaped := false
184
185 for ch in command.bytes() {
186 if escaped {
187 current << ch
188 escaped = false
189 continue
190 }
191 if ch == `\\` {
192 escaped = true
193 continue
194 }
195 if quote != 0 {
196 if ch == quote {
197 quote = 0
198 } else {
199 current << ch
200 }
201 continue
202 }
203 if ch == `"` || ch == `'` {
204 quote = ch
205 continue
206 }
207 if ch.is_space() {
208 if current.len > 0 {
209 args << current.bytestr()
210 current.clear()
211 }
212 continue
213 }
214 current << ch
215 }
216 if current.len > 0 {
217 args << current.bytestr()
218 }
219 return args
220}
221