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