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}