Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-08-19 00:11:35
Commitceeeb5229f041cde3b96a1ff27f91b403b205e98
Parent4a22b95d4b1a44bde5c239387430575ee063f9bb

Implement repository edit page

Diffstat

M main.go | 1 +
A res/repo/edit.html | 44 ++++++++++++++++++++++++++++++++++++++++++++
M res/repo/header.html | 3 +++
M res/res.go | 3 +++
M res/style.css | 12 ++++++------
M src/goit.go | 1 -
M src/http.go | 1 +
M src/repo.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------
M src/repo/commit.go | 4 +++-
M src/repo/edit.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
M src/repo/file.go | 8 +++++---
M src/repo/log.go | 4 +++-
M src/repo/refs.go | 4 +++-
M src/repo/repo.go | 5 +++++
M src/repo/tree.go | 8 +++++---

15 files changed, 200 insertions, 33 deletions

diff --git a/main.go b/main.go
index 6daa6f0..153a80b 100644
--- a/main.go
+++ b/main.go
@@ -46,6 +46,7 @@ func main() {
 	h.Path("/{repo}/tree/{path:.*}").Methods("GET").HandlerFunc(repo.HandleTree)
 	h.Path("/{repo}/file/{path:.*}").Methods("GET").HandlerFunc(repo.HandleFile)
 	h.Path("/{repo}/refs").Methods("GET").HandlerFunc(repo.HandleRefs)
+	h.Path("/{repo}/edit").Methods("GET", "POST").HandlerFunc(repo.HandleEdit)
 	h.Path("/{repo}/info/refs").Methods("GET").HandlerFunc(goit.HandleInfoRefs)
 	h.Path("/{repo}/git-upload-pack").Methods("POST").HandlerFunc(goit.HandleUploadPack)
 	h.Path("/{repo}/git-receive-pack").Methods("POST").HandlerFunc(goit.HandleReceivePack)
diff --git a/res/repo/edit.html b/res/repo/edit.html
new file mode 100644
index 0000000..f76b1d7
--- /dev/null
+++ b/res/repo/edit.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<head lang="en-GB">{{template "base/head" .}}</head>
+<body>
+	<header>{{template "repo/header" .}}</header><hr>
+	<main>
+		<h1>{{.Title}}</h1><hr>
+		<form action="/{{.Name}}/edit" method="post">
+			<table>
+				<tr>
+					<td style="text-align: right;"><label for="reponame">Name</label></td>
+					<td><input type="text" name="reponame" value="{{.Form.Name}}" spellcheck="false"></td>
+				</tr>
+				<tr>
+					<td style="text-align: right; vertical-align: top;"><label for="description">Description</label></td>
+					<td><textarea name="description" spellcheck="false">{{.Form.Description}}</textarea></td>
+				</tr>
+				<tr>
+					<td style="text-align: right;"><label for="visibility">Visibility</label></td>
+					<td>
+						<select name="visibility">
+							<option value="public">Public</option>
+							<option value="private" {{if .Form.IsPrivate}}selected{{end}}>Private</option>
+						</select>
+					</td>
+				</tr>
+				<tr>
+					<td></td>
+					<td>
+						<input type="submit" value="Update">
+						<a href="/{{.Name}}" style="color: inherit;">Cancel</a>
+					</td>
+				</tr>
+				<tr>
+					<td></td>
+					<td style="color: #AA0000">{{.Message}}</td>
+				</tr>
+			</table>
+		</form>
+		<table>
+			<tr><td style="text-align: right;"><span>ID:</span></td><td><span>{{.Form.Id}}</span></td></tr>
+			<tr><td style="text-align: right;"><span>Owner:</Span></td><td><span>{{.Form.Owner}}</span></td></tr>
+		</table>
+	</main>
+</body>
diff --git a/res/repo/header.html b/res/repo/header.html
index c59fbae..8391023 100644
--- a/res/repo/header.html
+++ b/res/repo/header.html
@@ -24,6 +24,9 @@
 			{{if .Licence}}
 				| <a href="{{.Licence}}">LICENCE</a>
 			{{end}}
+			{{if .Editable}}
+				| <a href="/{{.Name}}/edit">Edit</a>
+			{{end}}
 		</td>
 	</tr>
 </table>
diff --git a/res/res.go b/res/res.go
index 1e106ac..fbf0647 100644
--- a/res/res.go
+++ b/res/res.go
@@ -44,6 +44,9 @@ var RepoHeader string
 //go:embed repo/create.html
 var RepoCreate string
 
+//go:embed repo/edit.html
+var RepoEdit string
+
 //go:embed repo/log.html
 var RepoLog string
 
diff --git a/res/style.css b/res/style.css
index 2c00a7f..cb7f3a6 100644
--- a/res/style.css
+++ b/res/style.css
@@ -16,16 +16,16 @@ table td.lnum a { color: inherit; display: block; padding: 0 0.4rem 0 0.8rem; }
 table td.lnum a:hover { text-decoration: none; }
 table td.line { tab-size: 4; white-space: pre; }
 
-form table input { border: 2px solid #333333; border-radius: 3px; background-color: #111111; padding: 2px; }
-form table input[type="text"] { color: #888888; width: 24em; }
-form table input[type="password"] { color: #888888; width: 24em; }
-form table input[type="submit"] { color: #FF7E00; width: 6em; }
+table input { border: 2px solid #333333; border-radius: 3px; background-color: #111111; padding: 2px; }
+table input[type="text"] { color: #888888; width: 24em; }
+table input[type="password"] { color: #888888; width: 24em; }
+table input[type="submit"] { color: #FF7E00; width: 6em; }
 
-form table select {
+table select {
 	border: 2px solid #333333; border-radius: 3px; background-color: #111111; color: #888888; padding: 2px;
 	width: 24em;
 }
-form table textarea {
+table textarea {
 	border: 2px solid #333333; border-radius: 3px; background-color: #111111; color: #888888; max-height: 18rem;
 	min-height: 6rem; padding: 2px; resize: vertical; width: 24em;
 }
diff --git a/src/goit.go b/src/goit.go
index f9ff03d..b150df5 100644
--- a/src/goit.go
+++ b/src/goit.go
@@ -85,7 +85,6 @@ func Goit(conf string) (err error) {
 			name TEXT UNIQUE NOT NULL,
 			name_lower TEXT UNIQUE NOT NULL,
 			description TEXT NOT NULL,
-			default_branch TEXT NOT NULL,
 			is_private BOOLEAN NOT NULL
 		)`,
 	); err != nil {
diff --git a/src/http.go b/src/http.go
index d4e1137..8955ca3 100644
--- a/src/http.go
+++ b/src/http.go
@@ -31,6 +31,7 @@ func init() {
 
 	template.Must(Tmpl.New("repo/header").Parse(res.RepoHeader))
 	template.Must(Tmpl.New("repo/create").Parse(res.RepoCreate))
+	template.Must(Tmpl.New("repo/edit").Parse(res.RepoEdit))
 
 	template.Must(Tmpl.New("repo/log").Parse(res.RepoLog))
 	template.Must(Tmpl.New("repo/commit").Parse(res.RepoCommit))
diff --git a/src/repo.go b/src/repo.go
index afe986a..e523dd7 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -7,6 +7,7 @@ package goit
 import (
 	"database/sql"
 	"errors"
+	"log"
 	"os"
 	"strings"
 
@@ -14,20 +15,19 @@ import (
 )
 
 type Repo struct {
-	Id            int64
-	OwnerId       int64
-	Name          string
-	Description   string
-	DefaultBranch string
-	IsPrivate     bool
+	Id          int64
+	OwnerId     int64
+	Name        string
+	Description string
+	IsPrivate   bool
 }
 
 func GetRepo(id int64) (*Repo, error) {
 	r := &Repo{}
 
 	if err := db.QueryRow(
-		"SELECT id, owner_id, name, description, default_branch, is_private FROM repos WHERE id = ?", id,
-	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.IsPrivate); err != nil {
+		"SELECT id, owner_id, name, description, is_private FROM repos WHERE id = ?", id,
+	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil {
 		if !errors.Is(err, sql.ErrNoRows) {
 			return nil, err
 		}
@@ -42,8 +42,8 @@ func GetRepoByName(name string) (*Repo, error) {
 	r := &Repo{}
 
 	if err := db.QueryRow(
-		"SELECT id, owner_id, name, description, default_branch, is_private FROM repos WHERE name = ?", name,
-	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.IsPrivate); err != nil {
+		"SELECT id, owner_id, name, description, is_private FROM repos WHERE name = ?", name,
+	).Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil {
 		if !errors.Is(err, sql.ErrNoRows) {
 			return nil, err
 		}
@@ -61,9 +61,9 @@ func CreateRepo(repo Repo) error {
 	}
 
 	if _, err := tx.Exec(
-		`INSERT INTO repos (owner_id, name, name_lower, description, default_branch, is_private)
+		`INSERT INTO repos (owner_id, name, name_lower, description, is_private)
 		VALUES (?, ?, ?, ?, ?, ?)`,
-		repo.OwnerId, repo.Name, strings.ToLower(repo.Name), repo.Description, repo.DefaultBranch, repo.IsPrivate,
+		repo.OwnerId, repo.Name, strings.ToLower(repo.Name), repo.Description, repo.IsPrivate,
 	); err != nil {
 		tx.Rollback()
 		return err
@@ -107,3 +107,43 @@ func RepoExists(name string) (bool, error) {
 		return true, nil
 	}
 }
+
+func UpdateRepo(rid int64, repo Repo) error {
+	old, err := GetRepo(rid)
+	if err != nil {
+		return err
+	}
+
+	tx, err := db.Begin()
+	if err != nil {
+		return err
+	}
+
+	if _, err := tx.Exec(
+		"UPDATE repos SET name = ?, name_lower = ?, description = ?, is_private = ? WHERE id = ?",
+		repo.Name, strings.ToLower(repo.Name), repo.Description, repo.IsPrivate, rid,
+	); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	if repo.Name != old.Name {
+		/* TODO use a mutex lock or something to make sure this doesn't break */
+		if err := os.Rename(RepoPath(old.Name), RepoPath(repo.Name)); err != nil {
+			tx.Rollback()
+			return err
+		}
+	}
+
+	if err := tx.Commit(); err != nil {
+		os.Rename(RepoPath(repo.Name), RepoPath(old.Name))
+		log.Println("[repo/update]", "error while renaming, check repo \""+old.Name+"\"/\""+repo.Name+"\"")
+		return err
+	}
+
+	if _, err := db.Exec(""); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/src/repo/commit.go b/src/repo/commit.go
index 1058d3b..9ee34c7 100644
--- a/src/repo/commit.go
+++ b/src/repo/commit.go
@@ -45,9 +45,11 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 		Stats                         []stat
 		Summary                       string
 		Diff                          template.HTML
+		Editable                      bool
 	}{
 		Title: repo.Name + " - Log", Name: repo.Name, Description: repo.Description,
-		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable: (auth && repo.OwnerId == uid),
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))
diff --git a/src/repo/edit.go b/src/repo/edit.go
index b259065..2af4bce 100644
--- a/src/repo/edit.go
+++ b/src/repo/edit.go
@@ -1,10 +1,12 @@
 package repo
 
 import (
+	"fmt"
 	"log"
 	"net/http"
 
 	goit "github.com/Jamozed/Goit/src"
+	"github.com/Jamozed/Goit/src/util"
 	"github.com/gorilla/mux"
 )
 
@@ -21,9 +23,68 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// data := struct {
-	// 	Title string
-	// }{
-	// 	Title: "Repository - Edit",
-	// }
+	owner, err := goit.GetUser(repo.OwnerId)
+	if err != nil {
+		log.Println("[/repo/edit]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if owner == nil {
+		log.Println("[/repo/edit]", repo.Id, "is owned by a nonexistent user")
+		/* TODO have admin adopt the orphaned repository */
+		owner = &goit.User{}
+	}
+
+	data := struct {
+		Title, Name, Description, Url string
+		Readme, Licence, Message      string
+		Editable                      bool
+
+		Form struct {
+			Id, Owner, Name, Description string
+			IsPrivate                    bool
+		}
+	}{
+		Title:       "Repository - Edit",
+		Name:        repo.Name,
+		Description: repo.Description,
+		Url:         util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable:    (auth && repo.OwnerId == uid),
+	}
+
+	data.Form.Id = fmt.Sprint(repo.Id)
+	data.Form.Owner = owner.FullName + " (" + owner.Name + ")[" + fmt.Sprint(owner.Id) + "]"
+	data.Form.Name = repo.Name
+	data.Form.Description = repo.Description
+	data.Form.IsPrivate = repo.IsPrivate
+
+	if r.Method == http.MethodPost {
+		data.Form.Name = r.FormValue("reponame")
+		data.Form.Description = r.FormValue("description")
+		data.Form.IsPrivate = r.FormValue("visibility") == "private"
+
+		if data.Name == "" {
+			data.Message = "Name cannot be empty"
+		} else if util.SliceContains(reserved, data.Name) {
+			data.Message = "Name \"" + data.Name + "\" is reserved"
+		} else if exists, err := goit.RepoExists(data.Name); err != nil {
+			log.Println("[/repo/edit]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists && data.Name != repo.Name {
+			data.Message = "Name \"" + data.Name + "\" is taken"
+		} else if err := goit.UpdateRepo(repo.Id, goit.Repo{
+			Name: data.Form.Name, Description: data.Form.Description, IsPrivate: data.Form.IsPrivate,
+		}); err != nil {
+			log.Println("[/repo/edit]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else {
+			http.Redirect(w, r, "/"+data.Form.Name+"/edit", http.StatusFound)
+			return
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "repo/edit", data); err != nil {
+		log.Println("[/repo/edit]", err.Error())
+	}
 }
diff --git a/src/repo/file.go b/src/repo/file.go
index 09e3979..21eeecf 100644
--- a/src/repo/file.go
+++ b/src/repo/file.go
@@ -18,7 +18,7 @@ import (
 )
 
 func HandleFile(w http.ResponseWriter, r *http.Request) {
-	_, uid := goit.AuthCookie(w, r, true)
+	auth, uid := goit.AuthCookie(w, r, true)
 
 	treepath := mux.Vars(r)["path"]
 	// if treepath == "" {
@@ -30,7 +30,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
-	} else if repo == nil || (repo.IsPrivate && repo.OwnerId != uid) {
+	} else if repo == nil || (repo.IsPrivate && (!auth || repo.OwnerId != uid)) {
 		goit.HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -40,9 +40,11 @@ func HandleFile(w http.ResponseWriter, r *http.Request) {
 		Readme, Licence               string
 		Mode, File, Size              string
 		Lines                         []string
+		Editable                      bool
 	}{
 		Title: repo.Name + " - File", Name: repo.Name, Description: repo.Description,
-		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable: (auth && repo.OwnerId == uid),
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))
diff --git a/src/repo/log.go b/src/repo/log.go
index 03e46d0..1f667a3 100644
--- a/src/repo/log.go
+++ b/src/repo/log.go
@@ -44,9 +44,11 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 		Title, Name, Description, Url string
 		Readme, Licence               string
 		Commits                       []row
+		Editable                      bool
 	}{
 		Title: repo.Name + " - Log", Name: repo.Name, Description: repo.Description,
-		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable: (auth && repo.OwnerId == uid),
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))
diff --git a/src/repo/refs.go b/src/repo/refs.go
index 6bff474..164a51d 100644
--- a/src/repo/refs.go
+++ b/src/repo/refs.go
@@ -32,9 +32,11 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 		Title, Name, Description, Url string
 		Readme, Licence               string
 		Branches, Tags                []row
+		Editable                      bool
 	}{
 		Title: repo.Name + " - References", Name: repo.Name, Description: repo.Description,
-		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable: (auth && repo.OwnerId == uid),
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))
diff --git a/src/repo/repo.go b/src/repo/repo.go
index f118a21..cac89f8 100644
--- a/src/repo/repo.go
+++ b/src/repo/repo.go
@@ -13,6 +13,11 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/storer"
 )
 
+type HeaderFields struct {
+	Name, Description, Url, Readme, Licence string
+	Editable                                bool
+}
+
 var readmePattern = regexp.MustCompile(`(?i)^readme(?:\.?(?:md|txt))?$`)
 var licencePattern = regexp.MustCompile(`(?i)^licence(?:\.?(?:md|txt))?$`)
 
diff --git a/src/repo/tree.go b/src/repo/tree.go
index 8709225..7fd9c27 100644
--- a/src/repo/tree.go
+++ b/src/repo/tree.go
@@ -18,14 +18,14 @@ import (
 )
 
 func HandleTree(w http.ResponseWriter, r *http.Request) {
-	_, uid := goit.AuthCookie(w, r, true)
+	auth, uid := goit.AuthCookie(w, r, true)
 	treepath := mux.Vars(r)["path"]
 
 	repo, err := goit.GetRepoByName(mux.Vars(r)["repo"])
 	if err != nil {
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
-	} else if repo == nil || (repo.IsPrivate && repo.OwnerId != uid) {
+	} else if repo == nil || (repo.IsPrivate && (!auth || repo.OwnerId != uid)) {
 		goit.HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -38,9 +38,11 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 		Title, Name, Description, Url string
 		Readme, Licence               string
 		Files                         []row
+		Editable                      bool
 	}{
 		Title: repo.Name + " - Tree", Name: repo.Name, Description: repo.Description,
-		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable: (auth && repo.OwnerId == uid),
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))