package forge
import (
"errors"
"html/template"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
func installHook(root, host, dir string) {
binary, err := os.Executable()
if err != nil {
return
}
hooks := filepath.Join(dir, "hooks")
os.MkdirAll(hooks, 0o755)
writeHook(filepath.Join(hooks, "pre-receive"), hookScript(root, host, binary, "pre-receive"))
}
func (s *Server) installHooks() {
if err := installCentralHooks(s.root, s.host); err != nil {
log.Printf("hooks: %v", err)
}
owners, err := os.ReadDir(filepath.Join(s.root, "repos"))
if err != nil {
return
}
for _, o := range owners {
repos, _ := os.ReadDir(filepath.Join(s.root, "repos", o.Name()))
for _, r := range repos {
if strings.HasSuffix(r.Name(), ".git") {
installHook(s.root, s.host, filepath.Join(s.root, "repos", o.Name(), r.Name()))
}
}
}
}
func (g Git) Commit1(branch, parent, filePath string, content []byte, remove bool, message, name, email string) (string, error) {
index, err := os.CreateTemp("", "gitpocket-index")
if err != nil {
return "", err
}
index.Close()
defer os.Remove(index.Name())
env := []string{"GIT_INDEX_FILE=" + index.Name()}
if _, err := g.runEnv(env, nil, "read-tree", parent); err != nil {
return "", err
}
if remove {
if _, err := g.runEnv(env, []byte("0 "+strings.Repeat("0", 40)+"\t"+filePath+"\n"), "update-index", "--index-info"); err != nil {
return "", err
}
} else {
blob, err := g.run(content, "hash-object", "-w", "--stdin")
if err != nil {
return "", err
}
if _, err := g.runEnv(env, nil, "update-index", "--add", "--cacheinfo", "100644,"+strings.TrimSpace(string(blob))+","+filePath); err != nil {
return "", err
}
}
tree, err := g.runEnv(env, nil, "write-tree")
if err != nil {
return "", err
}
commit, err := g.runEnv(ident(name, email, timeNow()), []byte(message), "commit-tree", strings.TrimSpace(string(tree)), "-p", parent)
if err != nil {
return "", err
}
hash := strings.TrimSpace(string(commit))
if _, err := g.run(nil, "update-ref", "refs/heads/"+branch, hash, parent); err != nil {
return "", errors.New("the branch moved while you were editing, reload and try again")
}
return hash, nil
}
func (s *Server) editAllowed(w http.ResponseWriter, r *http.Request, repo Repo, branch string) (repoPage, Git, bool) {
rp, g, err := s.repoPage(r, repo, branch)
if err != nil {
s.fail(w, r, 404, "No such branch")
return rp, g, false
}
if rp.User == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return rp, g, false
}
if !rp.Owner {
s.fail(w, r, 403, "You cannot write to this repository")
return rp, g, false
}
if repo.Archived {
s.fail(w, r, 403, "This repository is archived")
return rp, g, false
}
if rp.Mirror != "" {
s.fail(w, r, 403, "This repository is a mirror and read only")
return rp, g, false
}
if _, err := g.Resolve("refs/heads/" + branch); err != nil || !validBranch(branch) {
s.fail(w, r, 404, "Edits need a branch, not a tag or commit")
return rp, g, false
}
if prot, ok := s.store.Protection(repo.ID, branch); ok && prot.RequirePR {
s.fail(w, r, 403, branch+" only accepts changes through pull requests")
return rp, g, false
}
rp.Tab = "tree"
return rp, g, true
}
func (s *Server) edit(w http.ResponseWriter, r *http.Request, repo Repo, spec string, create bool) {
g := Git{Dir: s.repoDir(repo)}
branch, filePath, ok := s.splitRef(g, spec)
if !ok {
s.fail(w, r, 404, "No such branch")
return
}
filePath, ok = cleanPath(filePath)
if !ok {
s.fail(w, r, 404, "No such path")
return
}
rp, g, ok := s.editAllowed(w, r, repo, branch)
if !ok {
return
}
data := map[string]any{"Page": rp, "Path": filePath, "Create": create, "Dir": filePath}
if r.Method == "GET" {
if !create {
if g.Kind(rp.Commit, filePath) != "blob" {
s.fail(w, r, 404, "No such file")
return
}
content, err := g.Blob(rp.Commit, filePath)
if err != nil || isBinary(content) || len(content) > maxRender {
s.fail(w, r, 400, "Only text files under 4 MiB can be edited here")
return
}
data["Content"] = string(content)
data["Message"] = "Update " + path.Base(filePath)
} else {
data["Message"] = "Create "
}
s.render(w, r, "edit", page{Title: "Edit " + filePath, Data: data})
return
}
target := filePath
if create {
name, ok := cleanPath(strings.TrimSpace(r.FormValue("name")))
if !ok || name == "" {
s.fail(w, r, 400, "Give the new file a name")
return
}
target = path.Join(filePath, name)
if g.Kind(rp.Commit, target) != "" {
s.fail(w, r, 400, target+" already exists")
return
}
}
content := strings.ReplaceAll(r.FormValue("content"), "\r\n", "\n")
message := strings.TrimSpace(r.FormValue("message"))
if message == "" {
message = "Update " + path.Base(target)
}
hash, err := g.Commit1(branch, rp.Commit, target, []byte(content), false, message, rp.User.Name, rp.User.Name+"@"+s.host)
if err != nil {
data["Content"], data["Message"], data["Name"] = r.FormValue("content"), message, r.FormValue("name")
w.WriteHeader(400)
s.render(w, r, "edit", page{Title: "Edit " + filePath, Error: err.Error(), Data: data})
return
}
_ = hash
http.Redirect(w, r, "/"+repo.Path()+"/tree/"+branch+"/"+target, http.StatusSeeOther)
}
func (s *Server) removeFile(w http.ResponseWriter, r *http.Request, repo Repo, spec string) {
g := Git{Dir: s.repoDir(repo)}
branch, filePath, ok := s.splitRef(g, spec)
if !ok {
s.fail(w, r, 404, "No such branch")
return
}
filePath, ok = cleanPath(filePath)
if !ok || filePath == "" {
s.fail(w, r, 404, "No such path")
return
}
rp, g, ok := s.editAllowed(w, r, repo, branch)
if !ok {
return
}
if g.Kind(rp.Commit, filePath) != "blob" {
s.fail(w, r, 404, "No such file")
return
}
if _, err := g.Commit1(branch, rp.Commit, filePath, nil, true, "Delete "+path.Base(filePath), rp.User.Name, rp.User.Name+"@"+s.host); err != nil {
s.fail(w, r, 400, template.HTMLEscapeString(err.Error()))
return
}
dir := path.Dir(filePath)
if dir == "." {
dir = ""
}
http.Redirect(w, r, "/"+repo.Path()+"/tree/"+branch+"/"+dir, http.StatusSeeOther)
}