package xrpc import ( "net/http" "path/filepath" "time" "unicode/utf8" "tangled.org/core/api/tangled" "tangled.org/core/appview/pages/markup" "tangled.org/core/knotserver/git" "tangled.org/core/types" xrpcerr "tangled.org/core/xrpc/errors" ) func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) { ctx := r.Context() repo := r.URL.Query().Get("repo") repoPath, err := x.parseRepoParam(repo) if err != nil { writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) return } ref := r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) path := r.URL.Query().Get("path") // path can be empty (defaults to root) gr, err := git.Open(repoPath, ref) if err != nil { x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref) writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) return } files, err := gr.FileTree(ctx, path) if err != nil { x.Logger.Error("failed to get file tree", "error", err, "path", path) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("PathNotFound"), xrpcerr.WithMessage("failed to read repository tree"), ), http.StatusNotFound) return } // if any of these files are a readme candidate, pass along its blob contents too var readmeFileName string var readmeContents string for _, file := range files { if markup.IsReadmeFile(file.Name) { contents, err := gr.RawContent(filepath.Join(path, file.Name)) if err != nil { x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name) } if utf8.Valid(contents) { readmeFileName = file.Name readmeContents = string(contents) break } } } // convert NiceTree -> tangled.RepoTree_TreeEntry treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files)) for i, file := range files { entry := &tangled.RepoTree_TreeEntry{ Name: file.Name, Mode: file.Mode, Size: file.Size, } if file.LastCommit != nil { entry.Last_commit = &tangled.RepoTree_LastCommit{ Hash: file.LastCommit.Hash.String(), Message: file.LastCommit.Message, When: file.LastCommit.When.Format(time.RFC3339), } } treeEntries[i] = entry } var parentPtr *string if path != "" { parentPtr = &path } var dotdotPtr *string if path != "" { dotdot := filepath.Dir(path) if dotdot != "." { dotdotPtr = &dotdot } } response := tangled.RepoTree_Output{ Ref: ref, Parent: parentPtr, Dotdot: dotdotPtr, Files: treeEntries, Readme: &tangled.RepoTree_Readme{ Filename: readmeFileName, Contents: readmeContents, }, } // calculate lastCommit for the directory as a whole var lastCommitTree *types.LastCommitInfo for _, e := range files { if e.LastCommit == nil { continue } if lastCommitTree == nil { lastCommitTree = e.LastCommit continue } if lastCommitTree.When.After(e.LastCommit.When) { lastCommitTree = e.LastCommit } } if lastCommitTree != nil { response.LastCommit = &tangled.RepoTree_LastCommit{ Hash: lastCommitTree.Hash.String(), Message: lastCommitTree.Message, When: lastCommitTree.When.Format(time.RFC3339), } // try to get author information commit, err := gr.Commit(lastCommitTree.Hash) if err == nil { response.LastCommit.Author = &tangled.RepoTree_Signature{ Name: commit.Author.Name, Email: commit.Author.Email, } } } writeJson(w, response) }