plz / repo / file_routes.v
296 lines · 246 sloc · 8.81 KB · 1fbeec1ebbd6a39fe565a31e861235ae2a78dd52
Raw
1module main
2
3import veb
4import os
5import git
6
7// GET /:username/:repo_name/new/:branch_name - Show create file form
8@['/:username/:repo_name/new/:branch_name']
9pub fn (mut app App) new_file(username string, repo_name string, branch_name string) veb.Result {
10 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
11
12 if !ctx.logged_in || repo.user_id != ctx.user.id {
13 return ctx.redirect_to_repository(username, repo_name)
14 }
15
16 default_content := ''
17 default_filename := ''
18 return $veb.html('templates/new_file.html')
19}
20
21// GET /:username/:repo_name/new-ci-file - Show create .gitly-ci.yml form (pre-filled)
22@['/:username/:repo_name/new-ci-file']
23pub fn (mut app App) new_ci_file(username string, repo_name string) veb.Result {
24 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
25
26 if !ctx.logged_in || repo.user_id != ctx.user.id {
27 return ctx.redirect_to_repository(username, repo_name)
28 }
29
30 branch_name := repo.primary_branch
31 default_filename := '.gitly-ci.yml'
32 default_content := 'jobs:
33 build:
34 steps:
35 - name: Build
36 run: echo "hello world"
37 - name: Test
38 run: echo "running tests"
39'
40 return $veb.html('templates/new_file.html')
41}
42
43// GET /:username/:repo_name/edit/:branch_name/:path... - Show edit file form
44@['/:username/:repo_name/edit/:branch_name/:path...']
45pub fn (mut app App) edit_file(username string, repo_name string, branch_name string, path string) veb.Result {
46 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
47
48 if !ctx.logged_in || repo.user_id != ctx.user.id {
49 return ctx.redirect_to_repository(username, repo_name)
50 }
51
52 file_content := repo.read_file(branch_name, path)
53
54 return $veb.html('templates/edit_file.html')
55}
56
57// POST /:username/:repo_name/update-file - Save edited file
58@['/:username/:repo_name/update-file'; post]
59pub fn (mut app App) handle_update_file(username string, repo_name string) veb.Result {
60 mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
61 return ctx.not_found()
62 }
63
64 if !ctx.logged_in || repo.user_id != ctx.user.id {
65 return ctx.redirect_to_repository(username, repo_name)
66 }
67
68 file_path := ctx.form['file_path']
69 file_content := ctx.form['file_content']
70 branch_name := ctx.form['branch']
71 commit_message := ctx.form['commit_message']
72 mut actual_branch := ''
73
74 if commit_message == '' {
75 ctx.error('Commit message is required')
76 path := file_path
77 return $veb.html('templates/edit_file.html')
78 }
79
80 actual_branch = branch_name
81 if actual_branch == '' {
82 actual_branch = repo.primary_branch
83 }
84
85 success := app.create_file_in_bare_repo(mut repo, actual_branch, file_path, file_content,
86 commit_message, ctx.user.username)
87
88 if !success {
89 ctx.error('Failed to save file')
90 path := file_path
91 return $veb.html('templates/edit_file.html')
92 }
93
94 // Clear cached files so the updated file shows up
95 app.delete_repository_files_in_branch(repo.id, actual_branch) or {}
96
97 app.update_repo_after_push(repo.id, actual_branch) or {
98 app.warn('Failed to update repo after file edit: ${err}')
99 }
100
101 // Trigger CI if applicable
102 if file_path == '.gitly-ci.yml' {
103 spawn app.trigger_ci_with_config(repo.id, actual_branch, file_content)
104 } else {
105 spawn app.trigger_ci_if_configured(repo.id, actual_branch)
106 }
107
108 return ctx.redirect('/${username}/${repo_name}/blob/${actual_branch}/${file_path}')
109}
110
111// POST /:username/:repo_name/create-file - Create a file in the repo
112@['/:username/:repo_name/create-file'; post]
113pub fn (mut app App) handle_create_file(username string, repo_name string) veb.Result {
114 mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
115 return ctx.not_found()
116 }
117
118 if !ctx.logged_in || repo.user_id != ctx.user.id {
119 return ctx.redirect_to_repository(username, repo_name)
120 }
121
122 file_path := ctx.form['file_path']
123 file_content := ctx.form['file_content']
124 branch_name := ctx.form['branch']
125 commit_message := ctx.form['commit_message']
126 mut actual_branch := ''
127
128 if file_path == '' {
129 ctx.error('File path is required')
130 default_content := file_content
131 default_filename := file_path
132 return $veb.html('templates/new_file.html')
133 }
134
135 if commit_message == '' {
136 ctx.error('Commit message is required')
137 default_content := file_content
138 default_filename := file_path
139 return $veb.html('templates/new_file.html')
140 }
141
142 // Sanitize file path
143 if file_path.contains('..') || file_path.contains('&') || file_path.contains(';') {
144 ctx.error('Invalid file path')
145 default_content := file_content
146 default_filename := file_path
147 return $veb.html('templates/new_file.html')
148 }
149
150 actual_branch = branch_name
151 if actual_branch == '' {
152 actual_branch = repo.primary_branch
153 }
154
155 success := app.create_file_in_bare_repo(mut repo, actual_branch, file_path, file_content,
156 commit_message, ctx.user.username)
157
158 if !success {
159 ctx.error('Failed to create file')
160 default_content := file_content
161 default_filename := file_path
162 return $veb.html('templates/new_file.html')
163 }
164
165 // Clear cached files so the new file shows up
166 app.delete_repository_files_in_branch(repo.id, actual_branch) or {}
167
168 // Update repo data
169 app.update_repo_after_push(repo.id, actual_branch) or {
170 app.warn('Failed to update repo after file creation: ${err}')
171 }
172
173 // Trigger CI — if we just created .gitly-ci.yml, pass the content directly
174 if file_path == '.gitly-ci.yml' {
175 spawn app.trigger_ci_with_config(repo.id, actual_branch, file_content)
176 } else {
177 spawn app.trigger_ci_if_configured(repo.id, actual_branch)
178 }
179
180 return ctx.redirect('/${username}/${repo_name}')
181}
182
183// Creates a file in a bare git repo using plumbing commands
184fn (mut app App) create_file_in_bare_repo(mut repo Repo, branch string, file_path string, content string, message string, author string) bool {
185 git_dir := repo.git_dir
186 app.info('Creating file ${file_path} in ${git_dir} on branch ${branch}')
187
188 // Write content to a temp file, then hash it into git
189 tmp_file := '/tmp/gitly_newfile_${repo.id}'
190 os.write_file(tmp_file, content) or {
191 app.warn('Failed to write temp file: ${err}')
192 return false
193 }
194 defer {
195 os.rm(tmp_file) or {}
196 }
197
198 // 1. Hash the blob
199 blob_hash := sh('git -C ${git_dir} hash-object -w ${tmp_file}')
200 if blob_hash == '' {
201 app.warn('hash-object failed')
202 return false
203 }
204
205 // 2. Read the current tree for this branch (if it exists)
206 mut parent_commit := ''
207 existing_tree := sh('git -C ${git_dir} rev-parse "${branch}^{tree}"')
208 has_existing_tree := existing_tree != ''
209
210 // Get parent commit hash
211 parent_commit = sh('git -C ${git_dir} rev-parse ${branch}')
212
213 // 3. Build a new tree
214 mut new_tree_hash := ''
215 if has_existing_tree {
216 tmp_index := '/tmp/gitly_index_${repo.id}'
217 defer {
218 os.rm(tmp_index) or {}
219 }
220
221 // Read existing tree into temp index
222 r1 :=
223 git.Git.exec_shell('GIT_INDEX_FILE=${tmp_index} git -C ${git_dir} read-tree ${existing_tree}')
224 if r1.exit_code != 0 {
225 app.warn('read-tree failed: ${r1.output}')
226 return false
227 }
228
229 // Add the new blob to the index
230 r2 :=
231 git.Git.exec_shell('GIT_INDEX_FILE=${tmp_index} git -C ${git_dir} update-index --add --cacheinfo 100644,${blob_hash},${file_path}')
232 if r2.exit_code != 0 {
233 app.warn('update-index failed: ${r2.output}')
234 return false
235 }
236
237 // Write the tree
238 r3 := git.Git.exec_shell('GIT_INDEX_FILE=${tmp_index} git -C ${git_dir} write-tree')
239 if r3.exit_code != 0 {
240 app.warn('write-tree failed: ${r3.output}')
241 return false
242 }
243 new_tree_hash = r3.output.trim_space()
244 } else {
245 // No existing tree — create from scratch using mktree
246 tree_entry := '100644 blob ${blob_hash}\t${file_path}'
247 tmp_tree := '/tmp/gitly_tree_${repo.id}'
248 os.write_file(tmp_tree, tree_entry + '\n') or { return false }
249 defer {
250 os.rm(tmp_tree) or {}
251 }
252 r := git.Git.exec_shell('git -C ${git_dir} mktree < ${tmp_tree}')
253 if r.exit_code != 0 {
254 app.warn('mktree failed: ${r.output}')
255 return false
256 }
257 new_tree_hash = r.output.trim_space()
258 }
259
260 if new_tree_hash == '' {
261 app.warn('Failed to create tree')
262 return false
263 }
264
265 // 4. Create a commit
266 mut parent_flag := ''
267 if parent_commit != '' {
268 parent_flag = '-p ${parent_commit}'
269 }
270
271 commit_sh := 'GIT_AUTHOR_NAME="${author}" GIT_AUTHOR_EMAIL="${author}@gitly" GIT_COMMITTER_NAME="${author}" GIT_COMMITTER_EMAIL="${author}@gitly" git -C ${git_dir} commit-tree ${new_tree_hash} ${parent_flag} -m "${message}"'
272 r4 := git.Git.exec_shell(commit_sh)
273 if r4.exit_code != 0 {
274 app.warn('commit-tree failed: ${r4.output}')
275 return false
276 }
277 new_commit_hash := r4.output.trim_space()
278
279 // 5. Update the branch ref
280 r5 := git.Git.exec_in_dir(git_dir, ['update-ref', 'refs/heads/${branch}', new_commit_hash])
281 if r5.exit_code != 0 {
282 app.warn('update-ref failed: ${r5.output}')
283 return false
284 }
285
286 app.info('File ${file_path} created with commit ${new_commit_hash}')
287 return true
288}
289
290fn sh(cmd string) string {
291 r := git.Git.exec_shell(cmd)
292 if r.exit_code != 0 {
293 return ''
294 }
295 return r.output.trim_space()
296}
297