readme file is now rendering in repo. can now view files and edit them. can switch between branches with dropdown menu
This commit is contained in:
@@ -160,6 +160,54 @@ func (h *RepoHandler) Blob(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, map[string]string{"content": string(content), "path": path, "ref": ref})
|
||||
}
|
||||
|
||||
func (h *RepoHandler) UpdateBlob(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
jsonError(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
var u models.User
|
||||
if has, _ := h.db.ID(userID).Get(&u); !has {
|
||||
jsonError(w, "user not found", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Message string `json:"message"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, "invalid body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Path == "" || req.Message == "" {
|
||||
jsonError(w, "path and message are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Branch == "" {
|
||||
req.Branch = repo.DefaultBranch
|
||||
}
|
||||
|
||||
authorEmail := u.Email
|
||||
if authorEmail == "" {
|
||||
authorEmail = u.Username + "@forgebucket.local"
|
||||
}
|
||||
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
if err := gitdomain.WriteFile(repo.DiskPath, req.Branch, req.Path, req.Content, u.Username, authorEmail, req.Message); err != nil {
|
||||
jsonError(w, "could not save file: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonOK(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Branches(w http.ResponseWriter, r *http.Request) {
|
||||
repo, ok := h.lookupRepo(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -109,6 +109,7 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, staticFi
|
||||
r.With(csrf).Delete("/", repoH.Delete)
|
||||
r.Get("/tree", repoH.Tree)
|
||||
r.Get("/blob", repoH.Blob)
|
||||
r.With(csrf).Put("/blob", repoH.UpdateBlob)
|
||||
r.Get("/commits", repoH.Commits)
|
||||
r.Get("/branches", repoH.Branches)
|
||||
r.Get("/diff", repoH.Diff)
|
||||
|
||||
@@ -3,6 +3,7 @@ package git
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -117,14 +118,17 @@ func Log(repoPath, branch string, limit int) ([]Commit, error) {
|
||||
}
|
||||
|
||||
type TreeEntry struct {
|
||||
Mode string `json:"mode"`
|
||||
Type string `json:"type"`
|
||||
Hash string `json:"hash"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
Type string `json:"type"`
|
||||
Hash string `json:"hash"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
CommitHash string `json:"commitHash"`
|
||||
CommitMsg string `json:"commitMsg"`
|
||||
CommitDate string `json:"commitDate"`
|
||||
}
|
||||
|
||||
func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) {
|
||||
// Short-circuit for repos with no commits yet.
|
||||
if IsEmpty(repoPath) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -132,7 +136,8 @@ func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) {
|
||||
if subPath != "" {
|
||||
treeRef = ref + ":" + subPath
|
||||
}
|
||||
out, err := run(repoPath, "ls-tree", treeRef)
|
||||
// -l adds size column: <mode> SP <type> SP <hash> SP <size> TAB <name>
|
||||
out, err := run(repoPath, "ls-tree", "-l", treeRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,26 +147,109 @@ func TreeLS(repoPath, ref, subPath string) ([]TreeEntry, error) {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// format: <mode> SP <type> SP <hash> TAB <name>
|
||||
tabIdx := strings.Index(line, "\t")
|
||||
if tabIdx < 0 {
|
||||
continue
|
||||
}
|
||||
name := line[tabIdx+1:]
|
||||
fields := strings.Fields(line[:tabIdx])
|
||||
if len(fields) != 3 {
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, TreeEntry{
|
||||
e := TreeEntry{
|
||||
Mode: fields[0],
|
||||
Type: fields[1],
|
||||
Hash: fields[2],
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
if fields[3] != "-" {
|
||||
fmt.Sscanf(fields[3], "%d", &e.Size)
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
|
||||
// Fetch last commit info for each entry.
|
||||
for i, e := range entries {
|
||||
filePath := e.Name
|
||||
if subPath != "" {
|
||||
filePath = subPath + "/" + e.Name
|
||||
}
|
||||
commitOut, err := run(repoPath, "log", "-1", "--format=%h\x1f%s\x1f%aI", "--", filePath)
|
||||
if err == nil {
|
||||
parts := strings.SplitN(strings.TrimSpace(string(commitOut)), "\x1f", 3)
|
||||
if len(parts) == 3 {
|
||||
entries[i].CommitHash = parts[0]
|
||||
entries[i].CommitMsg = parts[1]
|
||||
entries[i].CommitDate = parts[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// WriteFile writes content to filePath on branch inside a temporary worktree,
|
||||
// then commits with the given author and message. Uses git plumbing directly
|
||||
// so it works on bare repositories.
|
||||
func WriteFile(repoPath, branch, filePath, content, authorName, authorEmail, message string) error {
|
||||
clean := filepath.Clean(filepath.FromSlash(filePath))
|
||||
if strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
|
||||
return errors.New("invalid file path")
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "fb-edit-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("mktemp: %w", err)
|
||||
}
|
||||
|
||||
baseEnv := []string{"GIT_TERMINAL_PROMPT=0", "HOME=/tmp"}
|
||||
authorEnv := append(baseEnv,
|
||||
"GIT_AUTHOR_NAME="+authorName,
|
||||
"GIT_AUTHOR_EMAIL="+authorEmail,
|
||||
"GIT_COMMITTER_NAME="+authorName,
|
||||
"GIT_COMMITTER_EMAIL="+authorEmail,
|
||||
)
|
||||
|
||||
addWt := exec.Command("git", "worktree", "add", "--force", tmpDir, branch)
|
||||
addWt.Dir = filepath.Clean(repoPath)
|
||||
addWt.Env = baseEnv
|
||||
if out, err := addWt.CombinedOutput(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return fmt.Errorf("worktree add: %w: %s", err, out)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
rmWt := exec.Command("git", "worktree", "remove", "--force", tmpDir)
|
||||
rmWt.Dir = filepath.Clean(repoPath)
|
||||
rmWt.Env = baseEnv
|
||||
rmWt.Run()
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
fullPath := filepath.Join(tmpDir, clean)
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return fmt.Errorf("mkdirall: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("writefile: %w", err)
|
||||
}
|
||||
|
||||
addC := exec.Command("git", "add", clean)
|
||||
addC.Dir = tmpDir
|
||||
addC.Env = authorEnv
|
||||
if out, err := addC.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git add: %w: %s", err, out)
|
||||
}
|
||||
|
||||
commitC := exec.Command("git", "commit", "-m", message)
|
||||
commitC.Dir = tmpDir
|
||||
commitC.Env = authorEnv
|
||||
if out, err := commitC.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git commit: %w: %s", err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Branch struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user