Goit

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

AuthorJakob Wakeling <[email protected]>
Date2023-07-26 10:38:15
Commit20732d36486576a28971edc4b0b94b050805faae
Parent6f00d17f424fc5a475bb6121163d4b9b625e1e14

Properly create repositories through web interface

Diffstat

M main.go | 2 +-
M res/admin/repos.html | 2 +-
M res/admin/users.html | 2 +-
M res/repo/create.html | 47 ++++++++++++++++++++++++++++++++++-------------
M res/repo/log.html | 6 +++---
M res/style.css | 13 +++++++++++++
M src/repo.go | 95 +++++++++++++++++++++++++++++++++----------------------------------------------
A src/repo/create.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M src/repo/log.go | 22 ++++++++++------------
M src/repo/tree.go | 92 ++++++++++++++++++++++++++++++++++++++++---------------------------------------

10 files changed, 206 insertions, 131 deletions

diff --git a/main.go b/main.go
index 2beaf53..be5cb26 100644
--- a/main.go
+++ b/main.go
@@ -28,7 +28,7 @@ func main() {
 	h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin)
 	h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout)
 	h.Path("/user/sessions").Methods("GET", "POST").HandlerFunc(goit.HandleUserSessions)
-	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate)
+	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(repo.HandleCreate)
 	h.Path("/repo/delete").Methods("DELETE").HandlerFunc(repo.HandleDelete)
 	h.Path("/admin").Methods("GET").HandlerFunc(goit.HandleAdminIndex)
 	h.Path("/admin/users").Methods("GET").HandlerFunc(goit.HandleAdminUsers)
diff --git a/res/admin/repos.html b/res/admin/repos.html
index e2da387..a0e2997 100644
--- a/res/admin/repos.html
+++ b/res/admin/repos.html
@@ -11,7 +11,7 @@
 			</tr>
 			<tr><td>
 				<a href="/admin/users">Users</a>
-				| <a href="/admin/repos">Repos</a>
+				| <a href="/admin/repos">Repositories</a>
 			</td></tr>
 		</table>
 	</header><hr>
diff --git a/res/admin/users.html b/res/admin/users.html
index d37db37..acc0f0d 100644
--- a/res/admin/users.html
+++ b/res/admin/users.html
@@ -11,7 +11,7 @@
 			</tr>
 			<tr><td>
 				<a href="/admin/users">Users</a>
-				| <a href="/admin/repos">Repos</a>
+				| <a href="/admin/repos">Repositories</a>
 			</td></tr>
 		</table>
 	</header><hr>
diff --git a/res/repo/create.html b/res/repo/create.html
index 46641ad..7f5f495 100644
--- a/res/repo/create.html
+++ b/res/repo/create.html
@@ -12,17 +12,38 @@
 			<tr><td></td></tr>
 		</table>
 	</header>
-	<form action="/repo/create" method="post">
-		<label for="reponame">Name:</label>
-		<input type="text" name="reponame"><br>
-		<label for="description">Description</label>
-		<input type="text" name="description"><br>
-		<label for="visibility">Visibility:</label>
-		<select name="visibility">
-			<option value="public">Public</option>
-			<option value="private">Private</option>
-		</select><br>
-		<input type="submit" value="Create">
-	</form>
-	<p>{{.Message}}</p>
+	<main>
+		<form action="/repo/create" method="post">
+			<table>
+				<tr>
+					<td style="text-align: right;"><label for="reponame">Name</label></td>
+					<td><input type="text" name="reponame"></td>
+				</tr>
+				<tr>
+					<td style="text-align: right; vertical-align: top;"><label for="description">Description</label></td>
+					<td><textarea name="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">Private</option>
+						</select>
+					</td>
+				</tr>
+				<tr>
+					<td></td>
+					<td>
+						<input type="submit" value="Create">
+						<a href="/" style="color: inherit;">Cancel</a>
+					</td>
+				</tr>
+				<tr>
+					<td></td>
+					<td style="color: #AA0000">{{.Message}}</td>
+				</tr>
+			</table>
+		</form>
+	</main>
 </body>
diff --git a/res/repo/log.html b/res/repo/log.html
index f88337e..e814904 100644
--- a/res/repo/log.html
+++ b/res/repo/log.html
@@ -21,9 +21,9 @@
 							<td>{{.Date}}</a></td>
 							<td><a href="/{{$.Name}}/commit/{{.Hash}}">{{.Message}}</a></td>
 							<td>{{.Author}}</td>
-							<td align="right">{{.Files}}</td>
-							<td align="right" style="color: #008800">{{.Additions}}</td>
-							<td align="right" style="color: #AA0000">{{.Deletions}}</td>
+							<td style="text-align: right;">{{.Files}}</td>
+							<td style="text-align: right;" style="color: #008800">{{.Additions}}</td>
+							<td style="text-align: right;" style="color: #AA0000">{{.Deletions}}</td>
 						</tr>
 					{{end}}
 				{{else}}
diff --git a/res/style.css b/res/style.css
index e14b47f..6e14231 100644
--- a/res/style.css
+++ b/res/style.css
@@ -10,3 +10,16 @@ table td { padding: 0 0.4rem; }
 table td:empty::after { content: "\00a0"; }
 
 main table tr:hover td { background-color: #222222; }
+
+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="submit"] { color: #FF7E00; width: 6em; }
+
+form table select {
+	border: 2px solid #333333; border-radius: 3px; background-color: #111111; color: #888888; padding: 2px;
+	width: 24em;
+}
+form 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/repo.go b/src/repo.go
index 3deed7c..f6ea254 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -83,49 +83,6 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
-	ok, uid := AuthCookie(w, r, true)
-	if !ok {
-		HttpError(w, http.StatusUnauthorized)
-		return
-	}
-
-	data := struct{ Title, Message string }{Title: "Repository - Create"}
-
-	if r.Method == http.MethodPost {
-		reponame := r.FormValue("reponame")
-		description := r.FormValue("description")
-		private := r.FormValue("visibility") == "private"
-
-		if reponame == "" {
-			data.Message = "Name cannot be empty"
-		} else if util.SliceContains(reserved, reponame) {
-			data.Message = "Name \"" + reponame + "\" is reserved"
-		} else if exists, err := RepoExists(reponame); err != nil {
-			log.Println("[/repo/create]", err.Error())
-			HttpError(w, http.StatusInternalServerError)
-			return
-		} else if exists {
-			data.Message = "Name \"" + reponame + "\" already exists"
-		} else if _, err := db.Exec(
-			`INSERT INTO repos (owner_id, name, name_lower, description, default_branch, is_private)
-			VALUES (?, ?, ?, ?, ?, ?)`,
-			uid, reponame, strings.ToLower(reponame), description, "master", private,
-		); err != nil {
-			log.Println("[/repo/create]", err.Error())
-			HttpError(w, http.StatusInternalServerError)
-			return
-		} else {
-			http.Redirect(w, r, "/"+reponame, http.StatusFound)
-			return
-		}
-	}
-
-	if err := Tmpl.ExecuteTemplate(w, "repo/create", data); err != nil {
-		log.Println("[/repo/create]", err.Error())
-	}
-}
-
 func HandleRepoRefs(w http.ResponseWriter, r *http.Request) {
 	reponame := mux.Vars(r)["repo"]
 
@@ -216,6 +173,46 @@ func GetRepoByName(name string) (*Repo, error) {
 	return r, nil
 }
 
+func CreateRepo(repo Repo) error {
+	tx, err := db.Begin()
+	if err != nil {
+		return err
+	}
+
+	if _, err := tx.Exec(
+		`INSERT INTO repos (owner_id, name, name_lower, description, default_branch, is_private)
+		VALUES (?, ?, ?, ?, ?, ?)`,
+		repo.OwnerId, repo.Name, strings.ToLower(repo.Name), repo.Description, repo.DefaultBranch, repo.IsPrivate,
+	); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	if _, err := git.PlainInit(RepoPath(repo.Name), true); err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	if err := tx.Commit(); err != nil {
+		os.RemoveAll(RepoPath(repo.Name))
+		return err
+	}
+
+	return nil
+}
+
+func DelRepo(name string) error {
+	if err := os.RemoveAll(RepoPath(name)); err != nil {
+		return err
+	}
+
+	if _, err := db.Exec("DELETE FROM repos WHERE name = ?", name); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func RepoExists(name string) (bool, error) {
 	if err := db.QueryRow(
 		"SELECT name FROM repos WHERE name_lower = ?", strings.ToLower(name),
@@ -261,15 +258,3 @@ func RepoSize(name string) (uint64, error) {
 
 	return uint64(size), err
 }
-
-func DelRepo(name string) error {
-	if err := os.RemoveAll(RepoPath(name)); err != nil {
-		return err
-	}
-
-	if _, err := db.Exec("DELETE FROM repos WHERE name = ?", name); err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/src/repo/create.go b/src/repo/create.go
new file mode 100644
index 0000000..f0c158a
--- /dev/null
+++ b/src/repo/create.go
@@ -0,0 +1,56 @@
+package repo
+
+import (
+	"log"
+	"net/http"
+
+	goit "github.com/Jamozed/Goit/src"
+	"github.com/Jamozed/Goit/src/util"
+)
+
+var reserved []string = []string{"admin", "repo", "static", "user"}
+
+func HandleCreate(w http.ResponseWriter, r *http.Request) {
+	ok, uid := goit.AuthCookie(w, r, true)
+	if !ok {
+		goit.HttpError(w, http.StatusUnauthorized)
+		return
+	}
+
+	data := struct {
+		Title, Message    string
+		Name, Description string
+		IsPrivate         bool
+	}{Title: "Repository - Create"}
+
+	if r.Method == http.MethodPost {
+		data.Name = r.FormValue("reponame")
+		data.Description = r.FormValue("description")
+		data.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/create]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else if exists {
+			data.Message = "Name \"" + data.Name + "\" is taken"
+		} else if err := goit.CreateRepo(goit.Repo{
+			OwnerId: uid, Name: data.Name, Description: data.Description, IsPrivate: data.IsPrivate,
+		}); err != nil {
+			log.Println("[/repo/create]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		} else {
+			http.Redirect(w, r, "/"+data.Name, http.StatusFound)
+			return
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "repo/create", data); err != nil {
+		log.Println("[/repo/create]", err.Error())
+	}
+}
diff --git a/src/repo/log.go b/src/repo/log.go
index a971568..01f63ef 100644
--- a/src/repo/log.go
+++ b/src/repo/log.go
@@ -1,6 +1,7 @@
 package repo
 
 import (
+	"errors"
 	"fmt"
 	"log"
 	"net/http"
@@ -10,6 +11,7 @@ import (
 	goit "github.com/Jamozed/Goit/src"
 	"github.com/Jamozed/Goit/src/util"
 	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/gorilla/mux"
 )
@@ -53,21 +55,17 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	ref, err := gr.Head()
-	if err != nil {
-		log.Println("[/repo/log]", err.Error())
-		goit.HttpError(w, http.StatusInternalServerError)
-		return
-	}
-
-	iter, err := gr.Log(&git.LogOptions{From: ref.Hash()})
-	if err != nil {
+	if ref, err := gr.Head(); err != nil {
+		if !errors.Is(err, plumbing.ErrReferenceNotFound) {
+			log.Println("[/repo/log]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+	} else if iter, err := gr.Log(&git.LogOptions{From: ref.Hash()}); err != nil {
 		log.Println("[/repo/log]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
-	}
-
-	if err := iter.ForEach(func(c *object.Commit) error {
+	} else if err := iter.ForEach(func(c *object.Commit) error {
 		var files, additions, deletions int
 
 		if stats, err := c.Stats(); err != nil {
diff --git a/src/repo/tree.go b/src/repo/tree.go
index 3a61555..3bd72fa 100644
--- a/src/repo/tree.go
+++ b/src/repo/tree.go
@@ -12,6 +12,7 @@ import (
 	"github.com/Jamozed/Goit/src/util"
 	"github.com/dustin/go-humanize"
 	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/gorilla/mux"
 )
@@ -49,67 +50,68 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	ref, err := gr.Head()
-	if err != nil {
-		log.Println("[/repo/tree]", err.Error())
-		goit.HttpError(w, http.StatusInternalServerError)
-		return
-	}
-
-	commit, err := gr.CommitObject(ref.Hash())
-	if err != nil {
-		log.Println("[/repo/tree]", err.Error())
-		goit.HttpError(w, http.StatusInternalServerError)
-		return
-	}
-
-	tree, err := commit.Tree()
-	if err != nil {
-		log.Println("[/repo/tree]", err.Error())
-		goit.HttpError(w, http.StatusInternalServerError)
-		return
-	}
-
-	if treepath != "" {
-		data.Files = append(data.Files, row{Mode: "d---------", Name: "..", Path: path.Dir(treepath)})
-
-		tree, err = tree.Tree(treepath)
-		if errors.Is(err, object.ErrDirectoryNotFound) {
-			goit.HttpError(w, http.StatusNotFound)
+	if ref, err := gr.Head(); err != nil {
+		if !errors.Is(err, plumbing.ErrReferenceNotFound) {
+			log.Println("[/repo/tree]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
 			return
-		} else if err != nil {
+		}
+	} else {
+		commit, err := gr.CommitObject(ref.Hash())
+		if err != nil {
 			log.Println("[/repo/tree]", err.Error())
 			goit.HttpError(w, http.StatusInternalServerError)
 			return
 		}
-	}
 
-	sort.SliceStable(tree.Entries, func(i, j int) bool {
-		if tree.Entries[i].Mode&0o40000 != 0 && tree.Entries[j].Mode&0o40000 == 0 {
-			return true
+		tree, err := commit.Tree()
+		if err != nil {
+			log.Println("[/repo/tree]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
 		}
 
-		return tree.Entries[i].Name < tree.Entries[j].Name
-	})
+		if treepath != "" {
+			data.Files = append(data.Files, row{Mode: "d---------", Name: "..", Path: path.Dir(treepath)})
 
-	for _, v := range tree.Entries {
-		size := ""
-
-		if v.Mode&0o40000 == 0 {
-			file, err := tree.File(v.Name)
-			if err != nil {
+			tree, err = tree.Tree(treepath)
+			if errors.Is(err, object.ErrDirectoryNotFound) {
+				goit.HttpError(w, http.StatusNotFound)
+				return
+			} else if err != nil {
 				log.Println("[/repo/tree]", err.Error())
 				goit.HttpError(w, http.StatusInternalServerError)
 				return
 			}
-
-			size = humanize.IBytes(uint64(file.Size))
 		}
 
-		data.Files = append(data.Files, row{
-			Mode: util.ModeString(uint32(v.Mode)), Name: v.Name, Path: path.Join(treepath, v.Name), Size: size,
-			B: util.If(strings.HasSuffix(size, " B"), true, false),
+		sort.SliceStable(tree.Entries, func(i, j int) bool {
+			if tree.Entries[i].Mode&0o40000 != 0 && tree.Entries[j].Mode&0o40000 == 0 {
+				return true
+			}
+
+			return tree.Entries[i].Name < tree.Entries[j].Name
 		})
+
+		for _, v := range tree.Entries {
+			size := ""
+
+			if v.Mode&0o40000 == 0 {
+				file, err := tree.File(v.Name)
+				if err != nil {
+					log.Println("[/repo/tree]", err.Error())
+					goit.HttpError(w, http.StatusInternalServerError)
+					return
+				}
+
+				size = humanize.IBytes(uint64(file.Size))
+			}
+
+			data.Files = append(data.Files, row{
+				Mode: util.ModeString(uint32(v.Mode)), Name: v.Name, Path: path.Join(treepath, v.Name), Size: size,
+				B: util.If(strings.HasSuffix(size, " B"), true, false),
+			})
+		}
 	}
 
 	if err := goit.Tmpl.ExecuteTemplate(w, "repo/tree", data); err != nil {