1package knotserver
2
3import (
4 "compress/gzip"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "log"
12 "net/http"
13 "net/url"
14 "os"
15 "path/filepath"
16 "strconv"
17 "strings"
18
19 securejoin "github.com/cyphar/filepath-securejoin"
20 "github.com/gliderlabs/ssh"
21 "github.com/go-chi/chi/v5"
22 gogit "github.com/go-git/go-git/v5"
23 "github.com/go-git/go-git/v5/plumbing"
24 "github.com/go-git/go-git/v5/plumbing/object"
25 "tangled.sh/tangled.sh/core/knotserver/db"
26 "tangled.sh/tangled.sh/core/knotserver/git"
27 "tangled.sh/tangled.sh/core/patchutil"
28 "tangled.sh/tangled.sh/core/types"
29)
30
31func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
32 w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
33}
34
35func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
36 w.Header().Set("Content-Type", "application/json")
37
38 capabilities := map[string]any{
39 "pull_requests": map[string]any{
40 "format_patch": true,
41 "patch_submissions": true,
42 "branch_submissions": true,
43 "fork_submissions": true,
44 },
45 }
46
47 jsonData, err := json.Marshal(capabilities)
48 if err != nil {
49 http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
50 return
51 }
52
53 w.Write(jsonData)
54}
55
56func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
57 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
58 l := h.l.With("path", path, "handler", "RepoIndex")
59 ref := chi.URLParam(r, "ref")
60 ref, _ = url.PathUnescape(ref)
61
62 gr, err := git.Open(path, ref)
63 if err != nil {
64 log.Println(err)
65 if errors.Is(err, plumbing.ErrReferenceNotFound) {
66 resp := types.RepoIndexResponse{
67 IsEmpty: true,
68 }
69 writeJSON(w, resp)
70 return
71 } else {
72 l.Error("opening repo", "error", err.Error())
73 notFound(w)
74 return
75 }
76 }
77
78 commits, err := gr.Commits()
79 total := len(commits)
80 if err != nil {
81 writeError(w, err.Error(), http.StatusInternalServerError)
82 l.Error("fetching commits", "error", err.Error())
83 return
84 }
85 if len(commits) > 10 {
86 commits = commits[:10]
87 }
88
89 branches, err := gr.Branches()
90 if err != nil {
91 l.Error("getting branches", "error", err.Error())
92 writeError(w, err.Error(), http.StatusInternalServerError)
93 return
94 }
95
96 tags, err := gr.Tags()
97 if err != nil {
98 // Non-fatal, we *should* have at least one branch to show.
99 l.Warn("getting tags", "error", err.Error())
100 }
101
102 rtags := []*types.TagReference{}
103 for _, tag := range tags {
104 tr := types.TagReference{
105 Tag: tag.TagObject(),
106 }
107
108 tr.Reference = types.Reference{
109 Name: tag.Name(),
110 Hash: tag.Hash().String(),
111 }
112
113 if tag.Message() != "" {
114 tr.Message = tag.Message()
115 }
116
117 rtags = append(rtags, &tr)
118 }
119
120 var readmeContent string
121 var readmeFile string
122 for _, readme := range h.c.Repo.Readme {
123 content, _ := gr.FileContent(readme)
124 if len(content) > 0 {
125 readmeContent = string(content)
126 readmeFile = readme
127 }
128 }
129
130 files, err := gr.FileTree("")
131 if err != nil {
132 writeError(w, err.Error(), http.StatusInternalServerError)
133 l.Error("file tree", "error", err.Error())
134 return
135 }
136
137 if ref == "" {
138 mainBranch, err := gr.FindMainBranch()
139 if err != nil {
140 writeError(w, err.Error(), http.StatusInternalServerError)
141 l.Error("finding main branch", "error", err.Error())
142 return
143 }
144 ref = mainBranch
145 }
146
147 resp := types.RepoIndexResponse{
148 IsEmpty: false,
149 Ref: ref,
150 Commits: commits,
151 Description: getDescription(path),
152 Readme: readmeContent,
153 ReadmeFileName: readmeFile,
154 Files: files,
155 Branches: branches,
156 Tags: rtags,
157 TotalCommits: total,
158 }
159
160 writeJSON(w, resp)
161 return
162}
163
164func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
165 treePath := chi.URLParam(r, "*")
166 ref := chi.URLParam(r, "ref")
167 ref, _ = url.PathUnescape(ref)
168
169 l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
170
171 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
172 gr, err := git.Open(path, ref)
173 if err != nil {
174 notFound(w)
175 return
176 }
177
178 files, err := gr.FileTree(treePath)
179 if err != nil {
180 writeError(w, err.Error(), http.StatusInternalServerError)
181 l.Error("file tree", "error", err.Error())
182 return
183 }
184
185 resp := types.RepoTreeResponse{
186 Ref: ref,
187 Parent: treePath,
188 Description: getDescription(path),
189 DotDot: filepath.Dir(treePath),
190 Files: files,
191 }
192
193 writeJSON(w, resp)
194 return
195}
196
197func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
198 treePath := chi.URLParam(r, "*")
199 ref := chi.URLParam(r, "ref")
200 ref, _ = url.PathUnescape(ref)
201
202 l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
203
204 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
205 gr, err := git.Open(path, ref)
206 if err != nil {
207 notFound(w)
208 return
209 }
210
211 contents, err := gr.BinContent(treePath)
212 if err != nil {
213 writeError(w, err.Error(), http.StatusBadRequest)
214 l.Error("file content", "error", err.Error())
215 return
216 }
217
218 mimeType := http.DetectContentType(contents)
219
220 if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") {
221 l.Error("attempted to serve non-image/video file", "mimetype", mimeType)
222 writeError(w, "only image and video files can be accessed directly", http.StatusForbidden)
223 return
224 }
225
226 w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours
227 w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents)))
228 w.Header().Set("Content-Type", mimeType)
229 w.Write(contents)
230}
231
232func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
233 treePath := chi.URLParam(r, "*")
234 ref := chi.URLParam(r, "ref")
235 ref, _ = url.PathUnescape(ref)
236
237 l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
238
239 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
240 gr, err := git.Open(path, ref)
241 if err != nil {
242 notFound(w)
243 return
244 }
245
246 var isBinaryFile bool = false
247 contents, err := gr.FileContent(treePath)
248 if errors.Is(err, git.ErrBinaryFile) {
249 isBinaryFile = true
250 } else if errors.Is(err, object.ErrFileNotFound) {
251 notFound(w)
252 return
253 } else if err != nil {
254 writeError(w, err.Error(), http.StatusInternalServerError)
255 return
256 }
257
258 bytes := []byte(contents)
259 // safe := string(sanitize(bytes))
260 sizeHint := len(bytes)
261
262 resp := types.RepoBlobResponse{
263 Ref: ref,
264 Contents: string(bytes),
265 Path: treePath,
266 IsBinary: isBinaryFile,
267 SizeHint: uint64(sizeHint),
268 }
269
270 h.showFile(resp, w, l)
271}
272
273func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
274 name := chi.URLParam(r, "name")
275 file := chi.URLParam(r, "file")
276
277 l := h.l.With("handler", "Archive", "name", name, "file", file)
278
279 // TODO: extend this to add more files compression (e.g.: xz)
280 if !strings.HasSuffix(file, ".tar.gz") {
281 notFound(w)
282 return
283 }
284
285 ref := strings.TrimSuffix(file, ".tar.gz")
286
287 // This allows the browser to use a proper name for the file when
288 // downloading
289 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
290 setContentDisposition(w, filename)
291 setGZipMIME(w)
292
293 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
294 gr, err := git.Open(path, ref)
295 if err != nil {
296 notFound(w)
297 return
298 }
299
300 gw := gzip.NewWriter(w)
301 defer gw.Close()
302
303 prefix := fmt.Sprintf("%s-%s", name, ref)
304 err = gr.WriteTar(gw, prefix)
305 if err != nil {
306 // once we start writing to the body we can't report error anymore
307 // so we are only left with printing the error.
308 l.Error("writing tar file", "error", err.Error())
309 return
310 }
311
312 err = gw.Flush()
313 if err != nil {
314 // once we start writing to the body we can't report error anymore
315 // so we are only left with printing the error.
316 l.Error("flushing?", "error", err.Error())
317 return
318 }
319}
320
321func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
322 ref := chi.URLParam(r, "ref")
323 ref, _ = url.PathUnescape(ref)
324
325 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
326
327 l := h.l.With("handler", "Log", "ref", ref, "path", path)
328
329 gr, err := git.Open(path, ref)
330 if err != nil {
331 notFound(w)
332 return
333 }
334
335 commits, err := gr.Commits()
336 if err != nil {
337 writeError(w, err.Error(), http.StatusInternalServerError)
338 l.Error("fetching commits", "error", err.Error())
339 return
340 }
341
342 // Get page parameters
343 page := 1
344 pageSize := 30
345
346 if pageParam := r.URL.Query().Get("page"); pageParam != "" {
347 if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
348 page = p
349 }
350 }
351
352 if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
353 if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
354 pageSize = ps
355 }
356 }
357
358 // Calculate pagination
359 start := (page - 1) * pageSize
360 end := start + pageSize
361 total := len(commits)
362
363 if start >= total {
364 commits = []*object.Commit{}
365 } else {
366 if end > total {
367 end = total
368 }
369 commits = commits[start:end]
370 }
371
372 resp := types.RepoLogResponse{
373 Commits: commits,
374 Ref: ref,
375 Description: getDescription(path),
376 Log: true,
377 Total: total,
378 Page: page,
379 PerPage: pageSize,
380 }
381
382 writeJSON(w, resp)
383 return
384}
385
386func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
387 ref := chi.URLParam(r, "ref")
388 ref, _ = url.PathUnescape(ref)
389
390 l := h.l.With("handler", "Diff", "ref", ref)
391
392 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
393 gr, err := git.Open(path, ref)
394 if err != nil {
395 notFound(w)
396 return
397 }
398
399 diff, err := gr.Diff()
400 if err != nil {
401 writeError(w, err.Error(), http.StatusInternalServerError)
402 l.Error("getting diff", "error", err.Error())
403 return
404 }
405
406 resp := types.RepoCommitResponse{
407 Ref: ref,
408 Diff: diff,
409 }
410
411 writeJSON(w, resp)
412 return
413}
414
415func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
416 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
417 l := h.l.With("handler", "Refs")
418
419 gr, err := git.Open(path, "")
420 if err != nil {
421 notFound(w)
422 return
423 }
424
425 tags, err := gr.Tags()
426 if err != nil {
427 // Non-fatal, we *should* have at least one branch to show.
428 l.Warn("getting tags", "error", err.Error())
429 }
430
431 rtags := []*types.TagReference{}
432 for _, tag := range tags {
433 tr := types.TagReference{
434 Tag: tag.TagObject(),
435 }
436
437 tr.Reference = types.Reference{
438 Name: tag.Name(),
439 Hash: tag.Hash().String(),
440 }
441
442 if tag.Message() != "" {
443 tr.Message = tag.Message()
444 }
445
446 rtags = append(rtags, &tr)
447 }
448
449 resp := types.RepoTagsResponse{
450 Tags: rtags,
451 }
452
453 writeJSON(w, resp)
454 return
455}
456
457func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
458 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
459 l := h.l.With("handler", "Branches")
460
461 gr, err := git.Open(path, "")
462 if err != nil {
463 notFound(w)
464 return
465 }
466
467 branches, err := gr.Branches()
468 if err != nil {
469 l.Error("getting branches", "error", err.Error())
470 writeError(w, err.Error(), http.StatusInternalServerError)
471 return
472 }
473
474 resp := types.RepoBranchesResponse{
475 Branches: branches,
476 }
477
478 writeJSON(w, resp)
479 return
480}
481
482func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
483 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
484 branchName := chi.URLParam(r, "branch")
485 branchName, _ = url.PathUnescape(branchName)
486
487 l := h.l.With("handler", "Branch")
488
489 gr, err := git.PlainOpen(path)
490 if err != nil {
491 notFound(w)
492 return
493 }
494
495 ref, err := gr.Branch(branchName)
496 if err != nil {
497 l.Error("getting branch", "error", err.Error())
498 writeError(w, err.Error(), http.StatusInternalServerError)
499 return
500 }
501
502 commit, err := gr.Commit(ref.Hash())
503 if err != nil {
504 l.Error("getting commit object", "error", err.Error())
505 writeError(w, err.Error(), http.StatusInternalServerError)
506 return
507 }
508
509 defaultBranch, err := gr.FindMainBranch()
510 isDefault := false
511 if err != nil {
512 l.Error("getting default branch", "error", err.Error())
513 // do not quit though
514 } else if defaultBranch == branchName {
515 isDefault = true
516 }
517
518 resp := types.RepoBranchResponse{
519 Branch: types.Branch{
520 Reference: types.Reference{
521 Name: ref.Name().Short(),
522 Hash: ref.Hash().String(),
523 },
524 Commit: commit,
525 IsDefault: isDefault,
526 },
527 }
528
529 writeJSON(w, resp)
530 return
531}
532
533func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
534 l := h.l.With("handler", "Keys")
535
536 switch r.Method {
537 case http.MethodGet:
538 keys, err := h.db.GetAllPublicKeys()
539 if err != nil {
540 writeError(w, err.Error(), http.StatusInternalServerError)
541 l.Error("getting public keys", "error", err.Error())
542 return
543 }
544
545 data := make([]map[string]any, 0)
546 for _, key := range keys {
547 j := key.JSON()
548 data = append(data, j)
549 }
550 writeJSON(w, data)
551 return
552
553 case http.MethodPut:
554 pk := db.PublicKey{}
555 if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
556 writeError(w, "invalid request body", http.StatusBadRequest)
557 return
558 }
559
560 _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
561 if err != nil {
562 writeError(w, "invalid pubkey", http.StatusBadRequest)
563 }
564
565 if err := h.db.AddPublicKey(pk); err != nil {
566 writeError(w, err.Error(), http.StatusInternalServerError)
567 l.Error("adding public key", "error", err.Error())
568 return
569 }
570
571 w.WriteHeader(http.StatusNoContent)
572 return
573 }
574}
575
576func (h *Handle) NewRepo(w http.ResponseWriter, r *http.Request) {
577 l := h.l.With("handler", "NewRepo")
578
579 data := struct {
580 Did string `json:"did"`
581 Name string `json:"name"`
582 DefaultBranch string `json:"default_branch,omitempty"`
583 }{}
584
585 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
586 writeError(w, "invalid request body", http.StatusBadRequest)
587 return
588 }
589
590 if data.DefaultBranch == "" {
591 data.DefaultBranch = h.c.Repo.MainBranch
592 }
593
594 did := data.Did
595 name := data.Name
596 defaultBranch := data.DefaultBranch
597
598 relativeRepoPath := filepath.Join(did, name)
599 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
600 err := git.InitBare(repoPath, defaultBranch)
601 if err != nil {
602 l.Error("initializing bare repo", "error", err.Error())
603 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) {
604 writeError(w, "That repo already exists!", http.StatusConflict)
605 return
606 } else {
607 writeError(w, err.Error(), http.StatusInternalServerError)
608 return
609 }
610 }
611
612 // add perms for this user to access the repo
613 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
614 if err != nil {
615 l.Error("adding repo permissions", "error", err.Error())
616 writeError(w, err.Error(), http.StatusInternalServerError)
617 return
618 }
619
620 w.WriteHeader(http.StatusNoContent)
621}
622
623func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
624 l := h.l.With("handler", "RepoFork")
625
626 data := struct {
627 Did string `json:"did"`
628 Source string `json:"source"`
629 Name string `json:"name,omitempty"`
630 }{}
631
632 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
633 writeError(w, "invalid request body", http.StatusBadRequest)
634 return
635 }
636
637 did := data.Did
638 source := data.Source
639
640 if did == "" || source == "" {
641 l.Error("invalid request body, empty did or name")
642 w.WriteHeader(http.StatusBadRequest)
643 return
644 }
645
646 var name string
647 if data.Name != "" {
648 name = data.Name
649 } else {
650 name = filepath.Base(source)
651 }
652
653 relativeRepoPath := filepath.Join(did, name)
654 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
655
656 err := git.Fork(repoPath, source)
657 if err != nil {
658 l.Error("forking repo", "error", err.Error())
659 writeError(w, err.Error(), http.StatusInternalServerError)
660 return
661 }
662
663 // add perms for this user to access the repo
664 err = h.e.AddRepo(did, ThisServer, relativeRepoPath)
665 if err != nil {
666 l.Error("adding repo permissions", "error", err.Error())
667 writeError(w, err.Error(), http.StatusInternalServerError)
668 return
669 }
670
671 w.WriteHeader(http.StatusNoContent)
672}
673
674func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
675 l := h.l.With("handler", "RemoveRepo")
676
677 data := struct {
678 Did string `json:"did"`
679 Name string `json:"name"`
680 }{}
681
682 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
683 writeError(w, "invalid request body", http.StatusBadRequest)
684 return
685 }
686
687 did := data.Did
688 name := data.Name
689
690 if did == "" || name == "" {
691 l.Error("invalid request body, empty did or name")
692 w.WriteHeader(http.StatusBadRequest)
693 return
694 }
695
696 relativeRepoPath := filepath.Join(did, name)
697 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
698 err := os.RemoveAll(repoPath)
699 if err != nil {
700 l.Error("removing repo", "error", err.Error())
701 writeError(w, err.Error(), http.StatusInternalServerError)
702 return
703 }
704
705 w.WriteHeader(http.StatusNoContent)
706
707}
708func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
709 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
710
711 data := types.MergeRequest{}
712
713 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
714 writeError(w, err.Error(), http.StatusBadRequest)
715 h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
716 return
717 }
718
719 mo := &git.MergeOptions{
720 AuthorName: data.AuthorName,
721 AuthorEmail: data.AuthorEmail,
722 CommitBody: data.CommitBody,
723 CommitMessage: data.CommitMessage,
724 }
725
726 patch := data.Patch
727 branch := data.Branch
728 gr, err := git.Open(path, branch)
729 if err != nil {
730 notFound(w)
731 return
732 }
733
734 mo.FormatPatch = patchutil.IsFormatPatch(patch)
735
736 if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
737 var mergeErr *git.ErrMerge
738 if errors.As(err, &mergeErr) {
739 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
740 for i, conflict := range mergeErr.Conflicts {
741 conflicts[i] = types.ConflictInfo{
742 Filename: conflict.Filename,
743 Reason: conflict.Reason,
744 }
745 }
746 response := types.MergeCheckResponse{
747 IsConflicted: true,
748 Conflicts: conflicts,
749 Message: mergeErr.Message,
750 }
751 writeConflict(w, response)
752 h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
753 } else {
754 writeError(w, err.Error(), http.StatusBadRequest)
755 h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
756 }
757 return
758 }
759
760 w.WriteHeader(http.StatusOK)
761}
762
763func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
764 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
765
766 var data struct {
767 Patch string `json:"patch"`
768 Branch string `json:"branch"`
769 }
770
771 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
772 writeError(w, err.Error(), http.StatusBadRequest)
773 h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
774 return
775 }
776
777 patch := data.Patch
778 branch := data.Branch
779 gr, err := git.Open(path, branch)
780 if err != nil {
781 notFound(w)
782 return
783 }
784
785 err = gr.MergeCheck([]byte(patch), branch)
786 if err == nil {
787 response := types.MergeCheckResponse{
788 IsConflicted: false,
789 }
790 writeJSON(w, response)
791 return
792 }
793
794 var mergeErr *git.ErrMerge
795 if errors.As(err, &mergeErr) {
796 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
797 for i, conflict := range mergeErr.Conflicts {
798 conflicts[i] = types.ConflictInfo{
799 Filename: conflict.Filename,
800 Reason: conflict.Reason,
801 }
802 }
803 response := types.MergeCheckResponse{
804 IsConflicted: true,
805 Conflicts: conflicts,
806 Message: mergeErr.Message,
807 }
808 writeConflict(w, response)
809 h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
810 return
811 }
812 writeError(w, err.Error(), http.StatusInternalServerError)
813 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
814}
815
816func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
817 rev1 := chi.URLParam(r, "rev1")
818 rev1, _ = url.PathUnescape(rev1)
819
820 rev2 := chi.URLParam(r, "rev2")
821 rev2, _ = url.PathUnescape(rev2)
822
823 l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
824
825 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
826 gr, err := git.PlainOpen(path)
827 if err != nil {
828 notFound(w)
829 return
830 }
831
832 commit1, err := gr.ResolveRevision(rev1)
833 if err != nil {
834 l.Error("error resolving revision 1", "msg", err.Error())
835 writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
836 return
837 }
838
839 commit2, err := gr.ResolveRevision(rev2)
840 if err != nil {
841 l.Error("error resolving revision 2", "msg", err.Error())
842 writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
843 return
844 }
845
846 mergeBase, err := gr.MergeBase(commit1, commit2)
847 if err != nil {
848 l.Error("failed to find merge-base", "msg", err.Error())
849 writeError(w, "failed to calculate diff", http.StatusBadRequest)
850 return
851 }
852
853 rawPatch, formatPatch, err := gr.FormatPatch(mergeBase, commit2)
854 if err != nil {
855 l.Error("error comparing revisions", "msg", err.Error())
856 writeError(w, "error comparing revisions", http.StatusBadRequest)
857 return
858 }
859
860 writeJSON(w, types.RepoFormatPatchResponse{
861 Rev1: commit1.Hash.String(),
862 Rev2: commit2.Hash.String(),
863 FormatPatch: formatPatch,
864 Patch: rawPatch,
865 })
866 return
867}
868
869func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
870 l := h.l.With("handler", "NewHiddenRef")
871
872 forkRef := chi.URLParam(r, "forkRef")
873 forkRef, _ = url.PathUnescape(forkRef)
874
875 remoteRef := chi.URLParam(r, "remoteRef")
876 remoteRef, _ = url.PathUnescape(remoteRef)
877
878 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
879 gr, err := git.PlainOpen(path)
880 if err != nil {
881 notFound(w)
882 return
883 }
884
885 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
886 if err != nil {
887 l.Error("error tracking hidden remote ref", "msg", err.Error())
888 writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
889 return
890 }
891
892 w.WriteHeader(http.StatusNoContent)
893 return
894}
895
896func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
897 l := h.l.With("handler", "AddMember")
898
899 data := struct {
900 Did string `json:"did"`
901 }{}
902
903 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
904 writeError(w, "invalid request body", http.StatusBadRequest)
905 return
906 }
907
908 did := data.Did
909
910 if err := h.db.AddDid(did); err != nil {
911 l.Error("adding did", "error", err.Error())
912 writeError(w, err.Error(), http.StatusInternalServerError)
913 return
914 }
915 h.jc.AddDid(did)
916
917 if err := h.e.AddMember(ThisServer, did); err != nil {
918 l.Error("adding member", "error", err.Error())
919 writeError(w, err.Error(), http.StatusInternalServerError)
920 return
921 }
922
923 if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
924 l.Error("fetching and adding keys", "error", err.Error())
925 writeError(w, err.Error(), http.StatusInternalServerError)
926 return
927 }
928
929 w.WriteHeader(http.StatusNoContent)
930}
931
932func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
933 l := h.l.With("handler", "AddRepoCollaborator")
934
935 data := struct {
936 Did string `json:"did"`
937 }{}
938
939 ownerDid := chi.URLParam(r, "did")
940 repo := chi.URLParam(r, "name")
941
942 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
943 writeError(w, "invalid request body", http.StatusBadRequest)
944 return
945 }
946
947 if err := h.db.AddDid(data.Did); err != nil {
948 l.Error("adding did", "error", err.Error())
949 writeError(w, err.Error(), http.StatusInternalServerError)
950 return
951 }
952 h.jc.AddDid(data.Did)
953
954 repoName, _ := securejoin.SecureJoin(ownerDid, repo)
955 if err := h.e.AddCollaborator(data.Did, ThisServer, repoName); err != nil {
956 l.Error("adding repo collaborator", "error", err.Error())
957 writeError(w, err.Error(), http.StatusInternalServerError)
958 return
959 }
960
961 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
962 l.Error("fetching and adding keys", "error", err.Error())
963 writeError(w, err.Error(), http.StatusInternalServerError)
964 return
965 }
966
967 w.WriteHeader(http.StatusNoContent)
968}
969
970func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
971 l := h.l.With("handler", "DefaultBranch")
972 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
973
974 gr, err := git.Open(path, "")
975 if err != nil {
976 notFound(w)
977 return
978 }
979
980 branch, err := gr.FindMainBranch()
981 if err != nil {
982 writeError(w, err.Error(), http.StatusInternalServerError)
983 l.Error("getting default branch", "error", err.Error())
984 return
985 }
986
987 writeJSON(w, types.RepoDefaultBranchResponse{
988 Branch: branch,
989 })
990}
991
992func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
993 l := h.l.With("handler", "SetDefaultBranch")
994 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
995
996 data := struct {
997 Branch string `json:"branch"`
998 }{}
999
1000 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1001 writeError(w, err.Error(), http.StatusBadRequest)
1002 return
1003 }
1004
1005 gr, err := git.Open(path, "")
1006 if err != nil {
1007 notFound(w)
1008 return
1009 }
1010
1011 err = gr.SetDefaultBranch(data.Branch)
1012 if err != nil {
1013 writeError(w, err.Error(), http.StatusInternalServerError)
1014 l.Error("setting default branch", "error", err.Error())
1015 return
1016 }
1017
1018 w.WriteHeader(http.StatusNoContent)
1019}
1020
1021func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
1022 l := h.l.With("handler", "Init")
1023
1024 if h.knotInitialized {
1025 writeError(w, "knot already initialized", http.StatusConflict)
1026 return
1027 }
1028
1029 data := struct {
1030 Did string `json:"did"`
1031 }{}
1032
1033 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
1034 l.Error("failed to decode request body", "error", err.Error())
1035 writeError(w, "invalid request body", http.StatusBadRequest)
1036 return
1037 }
1038
1039 if data.Did == "" {
1040 l.Error("empty DID in request", "did", data.Did)
1041 writeError(w, "did is empty", http.StatusBadRequest)
1042 return
1043 }
1044
1045 if err := h.db.AddDid(data.Did); err != nil {
1046 l.Error("failed to add DID", "error", err.Error())
1047 writeError(w, err.Error(), http.StatusInternalServerError)
1048 return
1049 }
1050 h.jc.AddDid(data.Did)
1051
1052 if err := h.e.AddOwner(ThisServer, data.Did); err != nil {
1053 l.Error("adding owner", "error", err.Error())
1054 writeError(w, err.Error(), http.StatusInternalServerError)
1055 return
1056 }
1057
1058 if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
1059 l.Error("fetching and adding keys", "error", err.Error())
1060 writeError(w, err.Error(), http.StatusInternalServerError)
1061 return
1062 }
1063
1064 close(h.init)
1065
1066 mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
1067 mac.Write([]byte("ok"))
1068 w.Header().Add("X-Signature", hex.EncodeToString(mac.Sum(nil)))
1069
1070 w.WriteHeader(http.StatusNoContent)
1071}
1072
1073func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
1074 w.Write([]byte("ok"))
1075}