Patch demo

This Patch Does Not Apply To The Current Repository
Sarah@auth.lopeos.org submitted a patch request Feb 15, 2025 19:51
  • Title: Patch File Support
  • Author: Sarah Jamie Lewis <sarah@openprivacy.ca>

auth/indieauth.go

@@ -29,14 +29,16 @@ func NewAuthClient(config *common.Config) *AuthClient {
  	return &AuthClient{config: config, iac: iac, sessions: make(map[string]common.AuthInfo)}

              }

              

            -func (c *AuthClient) GetAuthInfo(r *http.Request) common.AuthInfo {

            +func (c *AuthClient) GetAuthInfo(config *common.Config, r *http.Request) common.AuthInfo {

              	cookie, err := r.Cookie(senaryAuthCookieName)

            -	user := common.AuthInfo{Name: "guest", Domain: "guest"}

            +	user := common.AuthInfo{Name: "guest", Domain: config.ClientID, IsMaintainer: false}

              	if cookie != nil && err == nil {

              		c.sessionMapLock.Lock()

              		if auth, exists := c.sessions[cookie.Value]; exists {

              			user.Name = auth.Name

              			user.Domain = auth.Domain

            +			user.IsMaintainer = config.IsMaintainer(auth.Domain)

            +			fmt.Printf("%v %v\n", user.IsMaintainer, auth.Domain)

              		}

              		c.sessionMapLock.Unlock()

              	}

            

common/auth.go

@@ -1,12 +1,21 @@
  package common

              

            -import "fmt"

            +import (

            +	"fmt"

            +	"strings"

            +)

              

              type AuthInfo struct {

            -	Name   string

            -	Domain string

            +	Name         string

            +	Domain       string

            +	IsMaintainer bool

              }

              

              func (ai AuthInfo) String() string {

            -	return fmt.Sprintf("%s@%s", ai.Name, ai.Domain)

            +	domain, isSecure := strings.CutPrefix(ai.Domain, "https://")

            +	if !isSecure {

            +		domain, _ = strings.CutPrefix(ai.Domain, "http://")

            +	}

            +	domain, _ = strings.CutSuffix(domain, "/")

            +	return fmt.Sprintf("%s@%s", ai.Name, domain)

              }

            

common/config.go

@@ -5,16 +5,22 @@ import (
  	"fmt"

              	"html/template"

              	"os"

            +	"slices"

              )

              

              type Config struct {

            -	BaseDir     string // the directory where static html/css files are stored

            -	RepoDir     string // the directory where static repo html files will be stored

            -	RequestsDir string // the directory where saved change requests will be stored

            -	ClientID    string

            -	CallbackURI string

            -	Templates   *template.Template

            -	CommitDepth int // the number of commits per repository to statically compile into pages (0 = all)

            +	BaseDir           string // the directory where static html/css files are stored

            +	RepoDir           string // the directory where static repo html files will be stored

            +	RequestsDir       string // the directory where saved change requests will be stored

            +	ClientID          string

            +	CallbackURI       string

            +	Templates         *template.Template

            +	CommitDepth       int // the number of commits per repository to statically compile into pages (0 = all)

            +	MaintainerDomains []string

            +}

            +

            +func (c *Config) IsMaintainer(domain string) bool {

            +	return slices.Contains(c.MaintainerDomains, domain)

              }

              

              func LoadConfig(path string) (*Config, error) {

            

common/request.go

@@ -2,15 +2,36 @@ package common
  

              import (

              	"encoding/json"

            +	"html/template"

              	"time"

              )

              

            +type RequestType int

            +

            +const (

            +	RequestInit = RequestType(iota)

            +	RequestReply

            +	RequestPatch

            +)

            +

            +type PatchInfo struct {

            +	Title  string

            +	Author string

            +	Body   template.HTML

            +}

            +

              type IssueRequest struct {

              	Summary     string

              	Description string

              	Created     time.Time

              	ID          string

              	User        AuthInfo

            +	PatchRef    string

            +	RequestType RequestType

            +	Approved    bool

            +	Moderated   bool

            +

            +	PatchInfo PatchInfo

              }

              

              func NewIssueRequest(summary string, description string, user AuthInfo) (IssueRequest, error) {

            
@@ -21,6 +42,7 @@ func NewIssueRequest(summary string, description string, user AuthInfo) (IssueRe
  		Created:     time.Now(),

              		ID:          ident,

              		User:        user,

            +		RequestType: RequestInit,

              	}, err

              }

              

            

gen/generate.go

@@ -16,7 +16,7 @@ import (
  )

              

              type StaticBase struct {

            -	User string

            +	User common.AuthInfo

              	Repo string

              }

              

            

go.mod

@@ -4,6 +4,11 @@ go 1.23.2
  

              require github.com/go-git/go-git/v5 v5.13.2

              

            +require (

            +	github.com/bluekeyes/go-gitdiff v0.8.1

            +	go.hacdias.com/indielib v0.4.3

            +)

            +

              require (

              	dario.cat/mergo v1.0.1 // indirect

              	github.com/Microsoft/go-winio v0.6.2 // indirect

            
@@ -20,7 +25,6 @@ require (
  	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect

              	github.com/skeema/knownhosts v1.3.0 // indirect

              	github.com/xanzy/ssh-agent v0.3.3 // indirect

            -	go.hacdias.com/indielib v0.4.3 // indirect

              	golang.org/x/crypto v0.33.0 // indirect

              	golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect

              	golang.org/x/net v0.35.0 // indirect

            

go.sum

@@ -10,6 +10,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
  github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=

              github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=

              github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=

            +github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI=

            +github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=

              github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=

              github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=

              github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=

            

repo/repo.go

@@ -24,7 +24,7 @@ func NewRepository(config *common.Config, repo string, ac *auth.AuthClient) *Rep
  }

              

              type StaticDir struct {

            -	User    string

            +	User    common.AuthInfo

              	Table   template.HTML

              	DirName string

              	Repo    string

            
@@ -56,9 +56,9 @@ func (repo *Repository) ServeTree(w http.ResponseWriter, r *http.Request) {
  		return

              	}

              

            -	user := repo.ac.GetAuthInfo(r)

            +	user := repo.ac.GetAuthInfo(repo.config, r)

              

            -	err = repo.config.Templates.ExecuteTemplate(w, "dir.tpl.html", StaticDir{Table: template.HTML(data), DirName: filepath.Dir(localFile), Repo: repo.repo, User: user.String()})

            +	err = repo.config.Templates.ExecuteTemplate(w, "dir.tpl.html", StaticDir{Table: template.HTML(data), DirName: filepath.Dir(localFile), Repo: repo.repo, User: user})

              	if err != nil {

              		http.Error(w, fmt.Errorf("file not found").Error(), http.StatusBadRequest)

              	}

            
@@ -0,0 +1,470 @@
+package repo

            +

            +import (

            +	"bytes"

            +	"encoding/json"

            +	"fmt"

            +	"io"

            +	"sync"

            +

            +	"html/template"

            +	"net/http"

            +	"os"

            +	"path"

            +	"path/filepath"

            +	"senary/auth"

            +	"senary/common"

            +	"slices"

            +	"strings"

            +

            +	"github.com/bluekeyes/go-gitdiff/gitdiff"

            +)

            +

            +type RequestList struct {

            +	User     common.AuthInfo

            +	Repo     string

            +	Requests []*common.IssueRequest

            +}

            +

            +type RequestManager struct {

            +	config *common.Config

            +	repo   string

            +	ac     *auth.AuthClient

            +	lock   sync.Mutex

            +}

            +

            +type RequestElement struct {

            +	Repo    string

            +	Request string

            +	User    common.AuthInfo

            +	Issue   common.IssueRequest

            +}

            +

            +type RequestThread struct {

            +	User     common.AuthInfo

            +	Repo     string

            +	Title    string

            +	ID       string

            +	Elements []template.HTML

            +}

            +

            +func NewRequestManager(config *common.Config, repo string, ac *auth.AuthClient) *RequestManager {

            +	return &RequestManager{config: config, repo: repo, ac: ac}

            +}

            +

            +func (rm *RequestManager) ServePatch(w http.ResponseWriter, r *http.Request) {

            +	idString := r.PathValue("id")

            +	patchHash := r.PathValue("patch")

            +	patchfile, err := os.Open(rm.RequestIssueElement(idString, patchHash))

            +	if err == nil {

            +		defer patchfile.Close()

            +		w.Header().Set("Content-Disposition", "attachment; filename="+patchHash+".patch")

            +		w.Header().Set("Content-Type", "text/x-diff")

            +		io.Copy(w, patchfile)

            +		return

            +	}

            +	http.Redirect(w, r, "/404", 404)

            +}

            +

            +func (rm *RequestManager) Serve(w http.ResponseWriter, r *http.Request) {

            +

            +	if strings.HasSuffix(r.URL.Path, "/new") {

            +		if value := r.FormValue("type"); value == "issue" {

            +			rm.config.Templates.ExecuteTemplate(w, "request.new.tpl.html", RequestList{Repo: rm.repo, User: rm.ac.GetAuthInfo(rm.config, r)})

            +			return

            +		}

            +		if value := r.FormValue("type"); value == "patch" {

            +			rm.config.Templates.ExecuteTemplate(w, "request.newpatch.tpl.html", RequestList{Repo: rm.repo, User: rm.ac.GetAuthInfo(rm.config, r)})

            +			return

            +		}

            +	}

            +

            +	// assume this is an issue thread...

            +	if !strings.HasSuffix(r.URL.Path, "/") {

            +		http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently)

            +	}

            +

            +	// list all issues

            +	if strings.HasSuffix(r.URL.Path, "requests/") {

            +		authuser := rm.ac.GetAuthInfo(rm.config, r)

            +		rl := RequestList{Repo: rm.repo, User: authuser}

            +

            +		// Get a list of issues in creation order...

            +		issueList := rm.LoadIssues(rm.RequestLogPath(), func(hash string) (*common.IssueRequest, error) {

            +			data, err := os.ReadFile(rm.RequestIssuePath(hash))

            +			if err == nil {

            +				var issue common.IssueRequest

            +				err := json.Unmarshal(data, &issue)

            +				return &issue, err

            +			}

            +			return nil, err

            +		},

            +			func(ir common.IssueRequest) bool {

            +				return authuser.IsMaintainer

            +			})

            +		slices.Reverse(issueList)

            +		rl.Requests = issueList

            +

            +		err := rm.config.Templates.ExecuteTemplate(w, "request.list.tpl.html", rl)

            +		if err != nil {

            +			fmt.Printf("error %v\n", err)

            +		}

            +		return

            +	}

            +

            +	// otherwise list a single issue...

            +	authuser := rm.ac.GetAuthInfo(rm.config, r)

            +	// otherwise...

            +	rt := RequestThread{Repo: rm.repo, User: rm.ac.GetAuthInfo(rm.config, r)}

            +	issuehash := path.Base(r.URL.Path)

            +	// Get a list of issues in creation order...

            +

            +	issueList := rm.LoadIssues(rm.RequestIssueLogPath(issuehash), func(hash string) (*common.IssueRequest, error) {

            +		data, err := os.ReadFile(rm.RequestIssueElement(issuehash, hash))

            +		if err == nil {

            +			var issue common.IssueRequest

            +			err := json.Unmarshal(data, &issue)

            +			return &issue, err

            +		}

            +		return nil, err

            +	},

            +		func(ir common.IssueRequest) bool {

            +			return ir.RequestType == common.RequestInit || authuser.IsMaintainer

            +		})

            +

            +	for _, issue := range issueList {

            +

            +		if issue.RequestType == common.RequestInit {

            +			rt.Title = issue.Summary

            +			rt.ID = issue.ID

            +		}

            +

            +		var doc bytes.Buffer

            +		rm.config.Templates.ExecuteTemplate(&doc, "request.element.tpl.html", RequestElement{Request: issuehash, Repo: rm.repo, Issue: *issue, User: authuser})

            +		rt.Elements = append(rt.Elements, template.HTML(doc.String()))

            +	}

            +

            +	err := rm.config.Templates.ExecuteTemplate(w, "request.thread.tpl.html", rt)

            +	if err != nil {

            +		fmt.Printf("error %v\n", err)

            +	}

            +}

            +

            +func (rm *RequestManager) LoadIssues(logpath string, loadfn func(hash string) (*common.IssueRequest, error), approve func(common.IssueRequest) bool) []*common.IssueRequest {

            +

            +	issueMap := make(map[string]*common.IssueRequest)

            +

            +	common.LogIer(logpath, func(le common.LogEntry) {

            +		//isFirst := issuehash == le.Hash

            +		if le.Type == common.LOG_APPROVED {

            +			issueMap[le.Hash].Approved = true

            +			issueMap[le.Hash].Moderated = false

            +		} else if le.Type == common.LOG_DELETED {

            +			issueMap[le.Hash].Approved = false

            +			issueMap[le.Hash].Moderated = false

            +		} else {

            +

            +			issue, err := loadfn(le.Hash)

            +			if err == nil {

            +

            +				issueMap[le.Hash] = issue

            +				issueMap[le.Hash].Moderated = true

            +				issueMap[le.Hash].Approved = approve(*issue)

            +

            +				if issue.PatchRef != "" {

            +					// NOTE: this currently takes advantage of the fact that patches can only be associated with a top

            +					// level issue, this is not the ideal long term...

            +					patchfile, err := os.Open(rm.RequestIssueElement(le.Hash, issue.PatchRef))

            +					if err == nil {

            +						files, prelude, err := gitdiff.Parse(patchfile)

            +						if err == nil {

            +							header, err := gitdiff.ParsePatchHeader(prelude)

            +							if err == nil {

            +								patchSummary := common.PatchInfo{}

            +								patchSummary.Title = header.Title

            +								if header.Author != nil {

            +									patchSummary.Author = header.Author.String()

            +								}

            +

            +								var doc bytes.Buffer

            +								rm.config.Templates.ExecuteTemplate(&doc, "diffstat.tpl.html", files)

            +								patchSummary.Body = template.HTML(doc.String())

            +								issue.PatchInfo = patchSummary

            +							}

            +						}

            +					}

            +				}

            +

            +			}

            +		}

            +	})

            +

            +	issueList := []*common.IssueRequest{}

            +

            +	for _, issue := range issueMap {

            +		if issue.Approved {

            +			issueList = append(issueList, issue)

            +		}

            +	}

            +

            +	slices.SortFunc(issueList, func(a *common.IssueRequest, b *common.IssueRequest) int {

            +		return a.Created.Compare(b.Created)

            +	})

            +

            +	return issueList

            +}

            +

            +func (rm *RequestManager) RequestLogPath() string {

            +	return filepath.Join(rm.config.RequestsDir, rm.repo, "log")

            +}

            +

            +func (rm *RequestManager) RequestIssuePath(issuehash string) string {

            +	return filepath.Join(rm.config.RequestsDir, rm.repo, issuehash, issuehash)

            +}

            +

            +func (rm *RequestManager) RequestIssueElement(issuehash string, subhash string) string {

            +	return filepath.Join(rm.config.RequestsDir, rm.repo, issuehash, subhash)

            +}

            +

            +func (rm *RequestManager) RequestIssueLogPath(issuehash string) string {

            +	return filepath.Join(rm.config.RequestsDir, rm.repo, issuehash, "log")

            +}

            +

            +func (rm *RequestManager) handleApprove(w http.ResponseWriter, r *http.Request) {

            +

            +	if rm.ac.GetAuthInfo(rm.config, r).IsMaintainer {

            +		id := r.FormValue("requestid")

            +		eid := r.FormValue("elementid")

            +

            +		if len(eid) == 0 {

            +			// approving a top-level

            +			err := rm.updateLog(rm.RequestLogPath(), id, common.LOG_APPROVED)

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +		} else {

            +			// approving a sub-level

            +			err := rm.updateLog(rm.RequestIssueLogPath(id), eid, common.LOG_APPROVED)

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +		}

            +	}

            +	http.Redirect(w, r, r.Header.Get("Referer"), 302)

            +}

            +

            +func (rm *RequestManager) handleTombstone(w http.ResponseWriter, r *http.Request) {

            +

            +	if rm.ac.GetAuthInfo(rm.config, r).IsMaintainer {

            +		id := r.FormValue("requestid")

            +		eid := r.FormValue("elementid")

            +

            +		if len(eid) == 0 {

            +			// approving a top-level

            +			err := rm.updateLog(rm.RequestLogPath(), id, common.LOG_DELETED)

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +		} else {

            +			// approving a sub-level

            +			err := rm.updateLog(rm.RequestIssueLogPath(id), eid, common.LOG_DELETED)

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +		}

            +	}

            +	http.Redirect(w, r, r.Header.Get("Referer"), 302)

            +}

            +

            +// thread safe logging (note: currently this means a global lock per repo for threads, this is likely fine for the target of smaller sites)

            +// as the locks are only used when recording hashes for new files/approvals/deletions

            +// TODO: in the future, break this down further to a lock per log?

            +func (rm *RequestManager) updateLog(logpath string, hash string, status common.LogType) error {

            +	rm.lock.Lock()

            +	defer rm.lock.Unlock()

            +	return common.WriteToLog(logpath, hash, status)

            +}

            +

            +func (rm *RequestManager) handlePatch(w http.ResponseWriter, r *http.Request) {

            +	// Parse our multipart form, 10 << 20 specifies a maximum

            +	// upload of 10 MB files.

            +	r.ParseMultipartForm(1024 * 1024 * 200)

            +	// FormFile returns the first file for the given key `myFile`

            +	// it also returns the FileHeader so we can get the Filename,

            +	// the Header and the size of the file

            +	file, _, err := r.FormFile("patchfile")

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +	defer file.Close()

            +

            +	id, err := common.RandomIdent()

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	fileBytes, err := io.ReadAll(file)

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	// TODO check that this is a patch file...

            +	buf := bytes.NewBuffer(fileBytes)

            +	_, description, err := gitdiff.Parse(buf)

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	summary := r.FormValue("summary")

            +	issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(rm.config, r))

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +	issueRequest.PatchRef = id

            +

            +	patchRef := filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, issueRequest.PatchRef)

            +	os.MkdirAll(filepath.Dir(patchRef), 0755)

            +	patchFile, _ := os.Create(patchRef)

            +	defer patchFile.Close()

            +	// write this byte array to our temporary file

            +	patchFile.Write(fileBytes)

            +

            +	rm.SaveIssue(&issueRequest, w, r)

            +

            +	http.Redirect(w, r, path.Join("repos", rm.repo, "requests", issueRequest.ID), http.StatusTemporaryRedirect)

            +}

            +

            +func (rm *RequestManager) SaveIssue(issueRequest *common.IssueRequest, w http.ResponseWriter, r *http.Request) {

            +	requestName := filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, issueRequest.ID)

            +	os.MkdirAll(filepath.Dir(requestName), 0755)

            +	err := os.WriteFile(requestName, issueRequest.Serialize(), 0644)

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	// create the parent entry of the requests log

            +	err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, "log"), issueRequest.ID, common.LOG_CREATED)

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	// finally write to our main requests log

            +	err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, "log"), issueRequest.ID, common.LOG_CREATED)

            +	if err != nil {

            +		http.Error(w, err.Error(), http.StatusBadRequest)

            +		return

            +	}

            +

            +	if issueRequest.User.IsMaintainer {

            +		// Auth Approve Maintainer Created Issues

            +		err = rm.updateLog(filepath.Join(rm.config.RequestsDir, rm.repo, "log"), issueRequest.ID, common.LOG_APPROVED)

            +		if err != nil {

            +			http.Error(w, err.Error(), http.StatusBadRequest)

            +			return

            +		}

            +

            +	}

            +

            +}

            +

            +func (rm *RequestManager) handleNew(w http.ResponseWriter, r *http.Request) {

            +

            +	summary := r.FormValue("summary")

            +	description := r.FormValue("description")

            +	replyto := r.FormValue("replyto")

            +	newtype := r.FormValue("type")

            +	if replyto == "" {

            +

            +		if newtype == "issue" {

            +			if len(summary) == 0 || len(description) == 0 {

            +				rm.Serve(w, r)

            +				return

            +			}

            +

            +			issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(rm.config, r))

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +

            +			rm.SaveIssue(&issueRequest, w, r)

            +			http.Redirect(w, r, path.Join("/repos", rm.repo, "requests", issueRequest.ID), http.StatusTemporaryRedirect)

            +		} else if newtype == "patch" {

            +			if len(summary) == 0 {

            +				rm.Serve(w, r)

            +				return

            +			}

            +			rm.handlePatch(w, r)

            +		}

            +	} else {

            +		// we are replying to an issue....

            +		if len(description) == 0 {

            +			rm.Serve(w, r)

            +			return

            +		}

            +		issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(rm.config, r))

            +		if err != nil {

            +			http.Error(w, err.Error(), http.StatusBadRequest)

            +			return

            +		}

            +		issueRequest.RequestType = common.RequestReply

            +

            +		requestName := filepath.Join(rm.config.RequestsDir, rm.repo, replyto, issueRequest.ID)

            +		err = os.WriteFile(requestName, issueRequest.Serialize(), 0644)

            +		if err != nil {

            +			http.Error(w, err.Error(), http.StatusBadRequest)

            +			return

            +		}

            +

            +		// create the parent entry of the requests log

            +		err = rm.updateLog(rm.RequestIssueLogPath(replyto), issueRequest.ID, common.LOG_CREATED)

            +		if err != nil {

            +			http.Error(w, err.Error(), http.StatusBadRequest)

            +			return

            +		}

            +

            +		if issueRequest.User.IsMaintainer {

            +			// Auth Approve Maintainer Created Issues

            +			err = rm.updateLog(rm.RequestIssueLogPath(replyto), issueRequest.ID, common.LOG_APPROVED)

            +			if err != nil {

            +				http.Error(w, err.Error(), http.StatusBadRequest)

            +				return

            +			}

            +

            +		}

            +

            +		http.Redirect(w, r, path.Join("/repos", rm.repo, "requests", replyto)+"/", http.StatusTemporaryRedirect)

            +		return

            +	}

            +}

            +

            +func (rm *RequestManager) Submit(w http.ResponseWriter, r *http.Request) {

            +

            +	if strings.HasSuffix(r.URL.Path, "/approve") {

            +		rm.handleApprove(w, r)

            +		return

            +	}

            +

            +	if strings.HasSuffix(r.URL.Path, "/tombstone") {

            +		rm.handleTombstone(w, r)

            +		return

            +	}

            +

            +	if strings.HasSuffix(r.URL.Path, "/new") {

            +		rm.handleNew(w, r)

            +		return

            +	}

            +

            +	http.Redirect(w, r, "/404", 404)

            +}

            

repo/serve.go Deleted

@@ -1,82 +0,0 @@
-package repo

            -

            -import (

            -	"net/http"

            -	"os"

            -	"path/filepath"

            -	"senary/auth"

            -	"senary/common"

            -	"strings"

            -)

            -

            -type RequestList struct {

            -	User     string

            -	Repo     string

            -	Requests []Request

            -}

            -

            -type Request struct {

            -	Summary string

            -}

            -

            -type RequestManager struct {

            -	config *common.Config

            -	repo   string

            -	ac     *auth.AuthClient

            -}

            -

            -func NewRequestManager(config *common.Config, repo string, ac *auth.AuthClient) *RequestManager {

            -	return &RequestManager{config: config, repo: repo, ac: ac}

            -}

            -

            -func (rm *RequestManager) Serve(w http.ResponseWriter, r *http.Request) {

            -

            -	if strings.HasSuffix(r.URL.Path, "/new") {

            -		if value := r.FormValue("type"); value == "issue" {

            -			rm.config.Templates.ExecuteTemplate(w, "request.new.tpl.html", RequestList{Repo: rm.repo, User: rm.ac.GetAuthInfo(r).String()})

            -			return

            -		}

            -	}

            -

            -	rm.config.Templates.ExecuteTemplate(w, "request.list.tpl.html", RequestList{Repo: rm.repo, User: rm.ac.GetAuthInfo(r).String()})

            -}

            -

            -func (rm *RequestManager) Submit(w http.ResponseWriter, r *http.Request) {

            -

            -	if strings.HasSuffix(r.URL.Path, "/new") {

            -		summary := r.FormValue("summary")

            -		description := r.FormValue("description")

            -		issueRequest, err := common.NewIssueRequest(summary, description, rm.ac.GetAuthInfo(r))

            -		if err != nil {

            -			http.Error(w, err.Error(), http.StatusBadRequest)

            -			return

            -		}

            -

            -		requestName := filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, issueRequest.ID)

            -		os.MkdirAll(filepath.Dir(requestName), 0755)

            -		err = os.WriteFile(requestName, issueRequest.Serialize(), 0644)

            -		if err != nil {

            -			http.Error(w, err.Error(), http.StatusBadRequest)

            -			return

            -		}

            -

            -		// create the parent entry of the requests log

            -		err = common.WriteToLog(filepath.Join(rm.config.RequestsDir, rm.repo, issueRequest.ID, "log"), issueRequest.ID, common.LOG_CREATED)

            -		if err != nil {

            -			http.Error(w, err.Error(), http.StatusBadRequest)

            -			return

            -		}

            -

            -		// finally write to our main requests log

            -		err = common.WriteToLog(filepath.Join(rm.config.RequestsDir, rm.repo, "log"), issueRequest.ID, common.LOG_CREATED)

            -		if err != nil {

            -			http.Error(w, err.Error(), http.StatusBadRequest)

            -			return

            -		}

            -

            -		rm.config.Templates.ExecuteTemplate(w, "request.new.tpl.html", RequestList{Repo: rm.repo, User: rm.ac.GetAuthInfo(r).String()})

            -		return

            -	}

            -

            -	http.Redirect(w, r, "/404", 404)

            -}

            

senary.go

@@ -81,6 +81,8 @@ func main() {
  		os.Exit(1)

              	}

              

            +	fmt.Printf("Maintainer Domains:%v\n", config.MaintainerDomains)

            +

              	switch os.Args[1] {

              	case "serve":

              

            
@@ -90,7 +92,7 @@ func main() {
  		http.Handle("/", fs)

              

              		http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {

            -			config.Templates.ExecuteTemplate(w, "index.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(r).String()}})

            +			config.Templates.ExecuteTemplate(w, "index.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(config, r)}})

              		})

              

              		http.HandleFunc("GET /repos/{$}", func(w http.ResponseWriter, r *http.Request) {

            
@@ -103,11 +105,11 @@ func main() {
  					entries = append(entries, gen.FileEntry{Dir: true, Name: e.Name(), Path: e.Name()})

              				}

              			}

            -			config.Templates.ExecuteTemplate(w, "repos.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(r).String()}, Path: "Repositories", DirName: "Repositories", Contents: entries})

            +			config.Templates.ExecuteTemplate(w, "repos.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(config, r)}, Path: "Repositories", DirName: "Repositories", Contents: entries})

              		})

              

              		http.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) {

            -			config.Templates.ExecuteTemplate(w, "login.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(r).String()}})

            +			config.Templates.ExecuteTemplate(w, "login.tpl.html", gen.StaticDirListing{StaticBase: gen.StaticBase{User: authClient.GetAuthInfo(config, r)}})

              		})

              		http.HandleFunc("POST /login", authClient.LoginHandler)

              		http.HandleFunc("GET /callback", authClient.CallbackHandler)

            
@@ -128,8 +130,11 @@ func main() {
  

              				// make a request handler for the repo

              				rm := repo.NewRequestManager(config, e.Name(), authClient)

            -				http.HandleFunc("GET "+path.Join("/repos/", e.Name(), "requests")+"/", rm.Serve)

            -				http.HandleFunc("POST "+path.Join("/repos/", e.Name(), "requests")+"/", rm.Submit)

            +				http.HandleFunc(path.Join("/repos/", e.Name(), "requests")+"/", rm.Serve)

            +				http.HandleFunc("POST "+path.Join("/repos/", e.Name(), "requests")+"/new", rm.Submit)

            +				http.HandleFunc("POST "+path.Join("/repos/", e.Name(), "requests")+"/approve", rm.Submit)

            +				http.HandleFunc("POST "+path.Join("/repos/", e.Name(), "requests")+"/tombstone", rm.Submit)

            +				http.HandleFunc("GET "+path.Join("/repos/", e.Name(), "patch/{id}/{patch}")+"/{$}", rm.ServePatch)

              				// allow https cloning by redirecting to git https backend

              				http.HandleFunc(path.Join("/repos/", e.Name()+".git")+"/", gitBackend)

              			}

            

static/css/custom.css

@@ -37,8 +37,31 @@
  

              .repomain {

                display: grid;

            -  grid-template-columns: 0.25fr 1fr;

            +  grid-template-columns: 20% 80%;

                grid-template-rows: 1fr;

                grid-column-gap: 0px;

                grid-row-gap: 0px;

            -  } 
            +  } 

            +

            +

            +  .shrink {

            +    flex: unset;

            +    margin-right:10px;

            +  }

            +

            +

            +

            +  .opadd {

            +    background-color: var(--pico-color-green-700);

            +  }

            +

            +  .opdelete {

            +    background-color:var(--pico-color-red-700);

            +  }

            +

            +.diff {

            +  width:100%;

            +  margin:auto;

            +  text-overflow: scroll;

            +  font-size: 0.75em;

            +}
            

templates/auth.success.tpl.html

@@ -1,11 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            +{{template "prelude.tpl.html" .}}

              

              <title>Senary</title>

              </head>

            
@@ -0,0 +1,27 @@
+{{range .}}

            +

            +    {{if eq .OldName .NewName}} 

            +        <h4>{{.NewName}}</h4>

            +    {{else if .IsDelete}}

            +            <!-- file has moved-->

            +        <h4>{{.OldName}} <mark>Deleted</mark></h4>

            +    {{else if .IsRename}}

            +        <h4>{{.OldName}} -> {{.NewName}} <mark>Renamed</mark></h4>

            +    {{end}}

            +

            +    {{$p := .}}

            +

            +    {{range .TextFragments}}

            +   

            +     <details {{if $p.IsDelete}}{{else}}open{{end}}>

            +        <summary>{{.Header}}</summary>

            +            <pre class="diff">{{range .Lines}}{{if eq .Op 2}}<span class="opadd">{{.}}</span>

            +            {{else if eq .Op 1}}<span class="opdelete">{{.}}</span>

            +            {{else}} <span>{{.}}</span>

            +            {{end}}{{end}}</pre>

            +    </details>

            +       

            +

            +    {{end}}

            +

            +{{end}}
            

templates/dir.tpl.html

@@ -1,11 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            +{{template "prelude.tpl.html" .}}

              

              <title>{{.DirName}} - Senary</title>

              </head>

            

templates/header.tpl.html

@@ -4,7 +4,7 @@
    </ul>

                <ul>

                  <li><a href="/repos">Repositories</a></li>

            -    <li><a href="/login">{{.User}}</a></li>

            +    <li><a href="/login">{{.User.String}}</a></li>

              

                  <li><style>

                    .theme-toggle{color:var(--pico-primary);}

            

templates/index.tpl.html

@@ -1,12 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            -

            +{{template "prelude.tpl.html" .}}

              <title>Repositories - Senary</title>

              </head>

              <body>

            

templates/login.tpl.html

@@ -1,11 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            +{{template "prelude.tpl.html" .}}

              

              <title>Authenticate with IndieAuth - Senary</title>

              </head>

            
@@ -0,0 +1,9 @@
+<!doctype html>

            +<html>

            +<head>

            +<link rel="stylesheet" href="/css/pico.min.css">

            +<link rel="stylesheet" href="/css/pico.colors.min.css">

            +<link rel="stylesheet" href="/css/custom.css">

            + <meta charset="utf-8">

            +    <meta name="viewport" content="width=device-width, initial-scale=1">

            +    <meta name="color-scheme" content="light dark">

            

templates/repos.tpl.html

@@ -1,11 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            +{{template "prelude.tpl.html" .}}

              

              <title>Repositories - Senary</title>

              </head>

            
@@ -0,0 +1,73 @@
+

            +{{if ne .Issue.PatchRef ""}}

            +

            +<article class="issueelement">

            +  <header>{{.Issue.User}}  submitted a <a href="/repos/{{.Repo}}/patch/{{.Issue.ID}}/{{.Issue.PatchRef}}">patch request</a> {{.Issue.Created.Format "Jan 02, 2006 15:04" }} 

            +

            +  </header>

            +  <ul>

            +      <li><strong>Title:</strong> {{.Issue.PatchInfo.Title}}</li>

            +      <li><strong>Author:</strong> {{.Issue.PatchInfo.Author}}</li>

            +    </ul>

            +        

            +     {{.Issue.PatchInfo.Body}}

            +       

            +

            +

            +

            +</article>

            +

            +

            +{{else if eq .Issue.RequestType 0}}

            +

            +<article class="issueelement">

            +  <header>{{.Issue.User}} commented {{.Issue.Created.Format "Jan 02, 2006 15:04" }} 

            +

            +  </header>

            +      <p>{{.Issue.Description}}</p>

            +

            +

            +

            +</article>

            +

            +{{else}}

            +

            +

            +<article class="issueelement">

            +  <header>{{.Issue.User}} replied {{.Issue.Created.Format "Jan 02, 2006 15:04" }}

            +

            +        

            +    {{if eq .Issue.Moderated true}}

            +     <mark>Pending</mark>

            +    {{end}}

            +  </header>

            +      <p>{{.Issue.Description}}</p>

            +

            +

            +      {{if eq .User.IsMaintainer true}} 

            +      <footer>

            +       <div role="group">

            +

            +         

            +         <form class="shrink" method="post" action="/repos/{{.Repo}}/requests/approve">

            +            <input hidden name="requestid" value="{{.Request}}" />

            +            <input hidden name="elementid" value="{{.Issue.ID}}" />

            +            <button data-tooltip="Approve Comment">✓</button>

            +          </form>

            +

            +          <form  class="shrink" method="post" action="/repos/{{.Repo}}/requests/tombstone">

            +            <input hidden name="requestid" value="{{.Request}}" />

            +            <input hidden name="elementid" value="{{.Issue.ID}}" />

            +            <button data-tooltip="Delete Comment">🗑</button>

            +          </form>

            +        </div>

            +      </footer>

            +      {{end}}

            +

            +</article>

            +

            +

            +{{end}}

            +

            +

            +

            

templates/request.list.tpl.html

@@ -1,12 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            -

            +{{template "prelude.tpl.html"}}

              <title>{{.Repo}} Change Requests - Senary</title>

              </head>

              <body>

            
@@ -18,13 +10,36 @@
  {{template "repomenu.tpl.html" .}}

              <div>

                  <section>

            -    <a href="./new?type=issue"><button>New Issue</button></a>

            -    <a href="./new?type=patch"><button>Submit Patch</button></a>

            +        <a href="/repos/{{.Repo}}/requests/new?type=issue"><button>New Issue</button></a>

            +    <a href="/repos/{{.Repo}}/requests/new?type=patch"><button>Submit Patch</button></a>

                  </section>

              <table class="striped">

            -<tr><th>Summary</th></tr>

            +<tr><th>Summary</th><th>Created</th>

            +{{if .User.IsMaintainer}}

            +<th>Maintainer Actions</th>

            +{{end}}

            +</tr>

            +{{$p := .}}

              {{range .Requests}}

            -    <tr><td>{{.Summary}}</td></tr>

            +    <tr><td><a href="./{{.ID}}">{{.Summary}}</a></td><td>{{.Created.Format "Jan 02, 2006 15:04" }}</td>

            +    

            +        {{if eq $p.User.IsMaintainer true}}

            +        <td  class="grid">

            +        

            +

            +         <form method="post" action="/repos/{{$p.Repo}}/requests/approve">

            +            <input hidden name="requestid" value="{{.ID}}" />

            +            <button>Approve</button>

            +          </form>

            +        

            +          <form method="post" action="/repos/{{$p.Repo}}/requests/tombstone">

            +            <input hidden name="requestid" value="{{.ID}}" />

            +            <button>Delete</button>

            +          </form>

            +

            +        </td>

            +        {{end}}

            +    </tr>

              {{end}}

              </table>

              </div>

            

templates/request.new.tpl.html

@@ -1,12 +1,4 @@
-<!doctype html>

            -<html>

            -<head>

            -<link rel="stylesheet" href="/css/pico.min.css">

            -<link rel="stylesheet" href="/css/custom.css">

            - <meta charset="utf-8">

            -    <meta name="viewport" content="width=device-width, initial-scale=1">

            -    <meta name="color-scheme" content="light dark">

            -

            +{{template "prelude.tpl.html" .}}

              <title>{{.Repo}} New Change Request - Senary</title>

              </head>

              <body>

            
@@ -17,6 +9,7 @@
  

              {{template "repomenu.tpl.html" .}}

              <div>

            +  <h2>Submit a New Change Request</h2>

              <form method="post" >

                <fieldset>

                  <label>

            
@@ -0,0 +1,45 @@
+{{template "prelude.tpl.html" .}}

            +

            +<title>{{.Repo}} New Change Request - Senary</title>

            +</head>

            +<body>

            +    <header class="container">

            +        {{ template "header.tpl.html" .}}

            +    </header>

            +<main class="container repomain">

            +

            +{{template "repomenu.tpl.html" .}}

            +<div>

            +  <h2>Submit a New Change Request</h2>

            +<form method="post" enctype="multipart/form-data" >

            +  <fieldset>

            +    <label>

            +      Summary

            +      <input

            +        name="summary"

            +        placeholder="A one line summary of the issue/request"

            +      />

            +    </label>

            +        <label>

            +      Patch File

            +      <input

            +        type="file"

            +        name="patchfile"

            +      />

            +    </label>

            +

            +  </fieldset>

            +

            +  <input

            +    type="submit"

            +    value="Suggest Issue"

            +  />

            +</form>

            +</div>

            +</main>

            +

            +<footer>

            +</footer>

            + 

            +</body>

            +</html>
            
@@ -0,0 +1,54 @@
+{{template "prelude.tpl.html" .}}

            +<title>{{.Repo}} Change Request - {{.Title}} - Senary</title>

            +</head>

            +<body>

            +    <header class="container">

            +        {{ template "header.tpl.html" .}}

            +    </header>

            +<main class="container repomain">

            +

            +{{template "repomenu.tpl.html" .}}

            +<div>

            +    <section>

            +    <a href="/repos/{{.Repo}}/requests/new?type=issue"><button>New Issue</button></a>

            +    <a href="/repos/{{.Repo}}/requests/new?type=patch"><button>Submit Patch</button></a>

            +    </section>

            +

            +<section>

            +    <span class=""> <h3>{{.Title}}</h3></span>

            +{{range .Elements}}

            +    {{.}}

            +{{end}}

            +

            +

            +<form method="post" action="/repos/{{.Repo}}/requests/new?type=reply">

            +     <fieldset role="group">

            +        <input

            +        name="replyto"

            +        hidden

            +        value="{{.ID}}"

            +      />

            +      <label>

            +        Reply

            +        <textarea

            +          name="description"

            +          placeholder="Add to this Change Request"

            +        ></textarea>

            +      </label>

            +    </fieldset>

            +    <input

            +      type="submit"

            +      value="Reply"

            +    />

            +  </form>

            +

            +

            +</section>

            +</div>

            +</main>

            +

            +<footer>

            +</footer>

            + 

            +</body>

            +</html>
            
Sarah@auth.lopeos.org replied Feb 15, 2025 19:53

Testing a Reply