From 1607d8fc9327e7b2bf20973c941d5e02ef1ef15c Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Tue, 14 Mar 2023 23:26:39 +0100 Subject: [PATCH] Added forks --- internal/git/commands.go | 103 ++++++++++++-------------------- internal/models/gist.go | 63 ++++++++++++++++--- internal/ssh/git-ssh.go | 6 +- internal/web/gist.go | 71 +++++++++++++++++++++- internal/web/git-http.go | 7 +-- internal/web/run.go | 2 + templates/base/gist_header.html | 26 ++++++++ templates/pages/all.html | 7 +++ templates/pages/forks.html | 36 +++++++++++ 9 files changed, 235 insertions(+), 86 deletions(-) create mode 100644 templates/pages/forks.html diff --git a/internal/git/commands.go b/internal/git/commands.go index 7329f9b..3fd0e1e 100644 --- a/internal/git/commands.go +++ b/internal/git/commands.go @@ -10,28 +10,21 @@ import ( "strings" ) -func GetRepositoryPath(user string, gist string) (string, error) { - return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist), nil +func RepositoryPath(user string, gist string) string { + return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist) } -func getTmpRepositoryPath(gistId string) (string, error) { - dirname, err := getTmpRepositoriesPath() - if err != nil { - return "", err - } - return filepath.Join(dirname, gistId), nil +func TmpRepositoryPath(gistId string) string { + dirname := TmpRepositoriesPath() + return filepath.Join(dirname, gistId) } -func getTmpRepositoriesPath() (string, error) { - return filepath.Join(config.GetHomeDir(), "tmp", "repos"), nil +func TmpRepositoriesPath() string { + return filepath.Join(config.GetHomeDir(), "tmp", "repos") } func InitRepository(user string, gist string) error { - repositoryPath, err := GetRepositoryPath(user, gist) - - if err != nil { - return err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( "git", @@ -40,7 +33,7 @@ func InitRepository(user string, gist string) error { repositoryPath, ) - _, err = cmd.Output() + _, err := cmd.Output() if err != nil { return err } @@ -69,10 +62,7 @@ func InitRepository(user string, gist string) error { } func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return "", err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( "git", @@ -87,10 +77,7 @@ func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) { } func GetFilesOfRepository(user string, gist string, commit string) ([]string, error) { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return nil, err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( "git", @@ -110,10 +97,7 @@ func GetFilesOfRepository(user string, gist string, commit string) ([]string, er } func GetFileContent(user string, gist string, commit string, filename string) (string, error) { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return "", err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( "git", @@ -128,10 +112,7 @@ func GetFileContent(user string, gist string, commit string, filename string) (s } func GetLog(user string, gist string, skip string) (string, error) { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return "", err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( "git", @@ -156,19 +137,13 @@ func GetLog(user string, gist string, skip string) (string, error) { } func CloneTmp(user string, gist string, gistTmpId string) error { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return err - } + repositoryPath := RepositoryPath(user, gist) - tmpPath, err := getTmpRepositoriesPath() - if err != nil { - return err - } + tmpPath := TmpRepositoriesPath() tmpRepositoryPath := path.Join(tmpPath, gistTmpId) - err = os.RemoveAll(tmpRepositoryPath) + err := os.RemoveAll(tmpRepositoryPath) if err != nil { return err } @@ -191,25 +166,33 @@ func CloneTmp(user string, gist string, gistTmpId string) error { return cmd.Run() } -func SetFileContent(gistTmpId string, filename string, content string) error { - repositoryPath, err := getTmpRepositoryPath(gistTmpId) - if err != nil { +func ForkClone(userSrc string, gistSrc string, userDst string, gistDst string) error { + repositoryPathSrc := RepositoryPath(userSrc, gistSrc) + repositoryPathDst := RepositoryPath(userDst, gistDst) + + cmd := exec.Command("git", "clone", "--bare", repositoryPathSrc, repositoryPathDst) + if err := cmd.Run(); err != nil { return err } + cmd = exec.Command("git", "config", "user.name", userDst) + cmd.Dir = repositoryPathDst + return cmd.Run() +} + +func SetFileContent(gistTmpId string, filename string, content string) error { + repositoryPath := TmpRepositoryPath(gistTmpId) + return os.WriteFile(filepath.Join(repositoryPath, filename), []byte(content), 0644) } func AddAll(gistTmpId string) error { - tmpPath, err := getTmpRepositoryPath(gistTmpId) - if err != nil { - return err - } + tmpPath := TmpRepositoryPath(gistTmpId) // in case of a change where only a file name has its case changed cmd := exec.Command("git", "rm", "-r", "--cached", "--ignore-unmatch", ".") cmd.Dir = tmpPath - err = cmd.Run() + err := cmd.Run() if err != nil { return err } @@ -222,27 +205,21 @@ func AddAll(gistTmpId string) error { func Commit(gistTmpId string) error { cmd := exec.Command("git", "commit", "--allow-empty", "-m", `"Opengist commit"`) - tmpPath, err := getTmpRepositoryPath(gistTmpId) - if err != nil { - return err - } + tmpPath := TmpRepositoryPath(gistTmpId) cmd.Dir = tmpPath return cmd.Run() } func Push(gistTmpId string) error { - tmpRepositoryPath, err := getTmpRepositoryPath(gistTmpId) - if err != nil { - return err - } + tmpRepositoryPath := TmpRepositoryPath(gistTmpId) cmd := exec.Command( "git", "push", ) cmd.Dir = tmpRepositoryPath - err = cmd.Run() + err := cmd.Run() if err != nil { return err } @@ -255,10 +232,7 @@ func DeleteRepository(user string, gist string) error { } func UpdateServerInfo(user string, gist string) error { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command("git", "update-server-info") cmd.Dir = repositoryPath @@ -266,10 +240,7 @@ func UpdateServerInfo(user string, gist string) error { } func RPCRefs(user string, gist string, service string) ([]byte, error) { - repositoryPath, err := GetRepositoryPath(user, gist) - if err != nil { - return nil, err - } + repositoryPath := RepositoryPath(user, gist) cmd := exec.Command("git", service, "--stateless-rpc", "--advertise-refs", ".") cmd.Dir = repositoryPath diff --git a/internal/models/gist.go b/internal/models/gist.go index a03f8f3..0f3bca6 100644 --- a/internal/models/gist.go +++ b/internal/models/gist.go @@ -16,10 +16,13 @@ type Gist struct { User User `validate:"-"` NbFiles int NbLikes int + NbForks int CreatedAt int64 UpdatedAt int64 - Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` + ForkedID uint Files []File `gorm:"-" validate:"min=1,dive"` } @@ -40,7 +43,7 @@ type Commit struct { func GetGist(user string, gistUuid string) (*Gist, error) { gist := new(Gist) - err := db.Preload("User"). + err := db.Preload("User").Preload("Forked.User"). Where("gists.uuid = ? AND users.username like ?", gistUuid, user). Joins("join users on gists.user_id = users.id"). First(&gist).Error @@ -50,7 +53,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) { func GetGistByID(gistId string) (*Gist, error) { gist := new(Gist) - err := db.Preload("User"). + err := db.Preload("User").Preload("Forked.User"). Where("gists.id = ?", gistId). First(&gist).Error @@ -59,7 +62,7 @@ func GetGistByID(gistId string) (*Gist, error) { func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { var gists []*Gist - err := db.Preload("User"). + err := db.Preload("User").Preload("Forked.User"). Where("gists.private = 0 or gists.user_id = ?", currentUserId). Limit(11). Offset(offset * 10). @@ -82,7 +85,7 @@ func GetAllGists(offset int) ([]*Gist, error) { func GetAllGistsFromUser(fromUser string, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { var gists []*Gist - err := db.Preload("User"). + err := db.Preload("User").Preload("Forked.User"). Where("users.username = ? and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", fromUser, currentUserId). Joins("join users on gists.user_id = users.id"). Limit(11). @@ -94,11 +97,16 @@ func GetAllGistsFromUser(fromUser string, currentUserId uint, offset int, sort s } func CreateGist(gist *Gist) error { + // avoids foreign key constraint error because the default value in the struct is 0 + return db.Omit("forked_id").Create(&gist).Error +} + +func CreateForkedGist(gist *Gist) error { return db.Create(&gist).Error } func UpdateGist(gist *Gist) error { - return db.Save(&gist).Error + return db.Omit("forked_id").Save(&gist).Error } func DeleteGist(gist *Gist) error { @@ -112,16 +120,40 @@ func GistLastActiveNow(gistID uint) error { } func AppendUserLike(gist *Gist, user *User) error { - db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1) + err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1).Error + if err != nil { + return err + } + return db.Model(&gist).Omit("updated_at").Association("Likes").Append(user) } func RemoveUserLike(gist *Gist, user *User) error { - db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1) + err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1).Error + if err != nil { + return err + } + return db.Model(&gist).Omit("updated_at").Association("Likes").Delete(user) } -func GetUsersLikesForGists(gist *Gist, offset int) ([]*User, error) { +func IncrementGistForkCount(gist *Gist) error { + return db.Model(&gist).Omit("updated_at").Update("nb_forks", gist.NbForks+1).Error +} + +func DecrementGistForkCount(gist *Gist) error { + return db.Model(&gist).Omit("updated_at").Update("nb_forks", gist.NbForks-1).Error +} + +func GetForkedGist(gist *Gist, user *User) (*Gist, error) { + fork := new(Gist) + err := db.Preload("User"). + Where("forked_id = ? and user_id = ?", gist.ID, user.ID). + First(&fork).Error + return fork, err +} + +func GetUsersLikesForGist(gist *Gist, offset int) ([]*User, error) { var users []*User err := db.Model(&gist). Where("gist_id = ?", gist.ID). @@ -131,6 +163,19 @@ func GetUsersLikesForGists(gist *Gist, offset int) ([]*User, error) { return users, err } +func GetUsersForksForGist(gist *Gist, currentUserId uint, offset int) ([]*Gist, error) { + var gists []*Gist + err := db.Model(&gist).Preload("User"). + Where("forked_id = ?", gist.ID). + Where("(gists.private = 0) or (gists.private = 1 and gists.user_id = ?)", currentUserId). + Limit(11). + Offset(offset * 10). + Order("updated_at desc"). + Find(&gists).Error + + return gists, err +} + func UserCanWrite(user *User, gist *Gist) bool { return !(user == nil) && (gist.UserID == user.ID) } diff --git a/internal/ssh/git-ssh.go b/internal/ssh/git-ssh.go index f7ddd05..d9c1093 100644 --- a/internal/ssh/git-ssh.go +++ b/internal/ssh/git-ssh.go @@ -53,11 +53,7 @@ func runGitCommand(ch ssh.Channel, gitCmd string, keyID uint) error { _ = models.SSHKeyLastUsedNow(keyID) - repositoryPath, err := git.GetRepositoryPath(gist.User.Username, gist.Uuid) - if err != nil { - errorSsh("Failed to get repository path", err) - return errors.New("internal server error") - } + repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid) cmd := exec.Command("git", verb, repositoryPath) cmd.Dir = repositoryPath diff --git a/internal/web/gist.go b/internal/web/gist.go index 77d06f6..c48dcb7 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -3,8 +3,10 @@ package web import ( "archive/zip" "bytes" + "errors" "github.com/google/uuid" "github.com/labstack/echo/v4" + "gorm.io/gorm" "html/template" "net/url" "opengist/internal/config" @@ -413,6 +415,49 @@ func like(ctx echo.Context) error { return redirect(ctx, redirectTo) } +func fork(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + currentUser := getUserLogged(ctx) + + alreadyForked, err := models.GetForkedGist(gist, currentUser) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return errorRes(500, "Error checking if gist is already forked", err) + } + + if alreadyForked.ID != 0 { + return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid) + } + + uuidGist, err := uuid.NewRandom() + if err != nil { + return errorRes(500, "Error creating an UUID", err) + } + + newGist := &models.Gist{ + Uuid: strings.Replace(uuidGist.String(), "-", "", -1), + Title: gist.Title, + Preview: gist.Preview, + PreviewFilename: gist.PreviewFilename, + Description: gist.Description, + Private: gist.Private, + UserID: currentUser.ID, + ForkedID: gist.ID, + } + + if err = models.CreateForkedGist(newGist); err != nil { + return errorRes(500, "Error forking the gist in database", err) + } + + if err = git.ForkClone(gist.User.Username, gist.Uuid, currentUser.Username, newGist.Uuid); err != nil { + return errorRes(500, "Error cloning the repository while forking", err) + } + if err = models.IncrementGistForkCount(gist); err != nil { + return errorRes(500, "Error incrementing the fork count", err) + } + + return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Uuid) +} + func rawFile(ctx echo.Context) error { gist := getData(ctx, "gist").(*models.Gist) fileContent, err := git.GetFileContent( @@ -505,7 +550,7 @@ func likes(ctx echo.Context) error { pageInt := getPage(ctx) - likers, err := models.GetUsersLikesForGists(gist, pageInt-1) + likers, err := models.GetUsersLikesForGist(gist, pageInt-1) if err != nil { return errorRes(500, "Error getting users who liked this gist", err) } @@ -518,3 +563,27 @@ func likes(ctx echo.Context) error { setData(ctx, "revision", "HEAD") return html(ctx, "likes.html") } + +func forks(ctx echo.Context) error { + var gist = getData(ctx, "gist").(*models.Gist) + pageInt := getPage(ctx) + + currentUser := getUserLogged(ctx) + var fromUserID uint = 0 + if currentUser != nil { + fromUserID = currentUser.ID + } + + forks, err := models.GetUsersForksForGist(gist, fromUserID, pageInt-1) + if err != nil { + return errorRes(500, "Error getting users who liked this gist", err) + } + + if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks"); err != nil { + return errorRes(404, "Page not found", nil) + } + + setData(ctx, "htmlTitle", "Forks for "+gist.Title) + setData(ctx, "revision", "HEAD") + return html(ctx, "forks.html") +} diff --git a/internal/web/git-http.go b/internal/web/git-http.go index e5b5f39..40399b8 100644 --- a/internal/web/git-http.go +++ b/internal/web/git-http.go @@ -50,12 +50,9 @@ func gitHttp(ctx echo.Context) error { strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") || ctx.Request().Method == "GET" - repositoryPath, err := git.GetRepositoryPath(gist.User.Username, gist.Uuid) - if err != nil { - return errorRes(500, "Cannot get repository path", err) - } + repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid) - if _, err = os.Stat(repositoryPath); os.IsNotExist(err) { + if _, err := os.Stat(repositoryPath); os.IsNotExist(err) { if err != nil { return errorRes(500, "Repository does not exist", err) } diff --git a/internal/web/run.go b/internal/web/run.go index 3a2835a..dfa6045 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -159,6 +159,8 @@ func Start() { g3.POST("/edit", processCreate, logged, writePermission) g3.POST("/like", like, logged) g3.GET("/likes", likes) + g3.POST("/fork", fork, logged) + g3.GET("/forks", forks) } } diff --git a/templates/base/gist_header.html b/templates/base/gist_header.html index c00b31d..ae579b9 100644 --- a/templates/base/gist_header.html +++ b/templates/base/gist_header.html @@ -26,6 +26,18 @@ {{ .gist.NbLikes }} +
+ {{ .csrfHtml }} + + + {{ .gist.NbForks }} + +
{{ else }}
@@ -38,6 +50,17 @@ {{ .gist.NbLikes }}
+
+ + + + + Fork + + + {{ .gist.NbForks }} + +
{{ end }} {{ if .userLogged }}{{ if eq .gist.User.Username .userLogged.Username }}
@@ -79,6 +102,9 @@ {{ end }}{{ end }} + {{ if .gist.Forked }} +

Forked from {{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}

+ {{ end }}

Last active {{ .gist.UpdatedAt }} • {{ if .gist.Private }} Unlisted {{else}} Public {{ end }} diff --git a/templates/pages/all.html b/templates/pages/all.html index c2860c5..fbb9360 100644 --- a/templates/pages/all.html +++ b/templates/pages/all.html @@ -55,6 +55,12 @@ {{ $gist.NbLikes }} likes +

+ + + + {{ $gist.NbForks }} forks +
@@ -65,6 +71,7 @@
Last active {{ $gist.UpdatedAt }} + {{ if $gist.Forked }} • Forked from {{ $gist.Forked.User.Username }}/{{ $gist.Forked.Title }} {{ end }} {{ if $gist.Private }} • Unlisted {{ end }}
{{ $gist.Description }}
diff --git a/templates/pages/forks.html b/templates/pages/forks.html new file mode 100644 index 0000000..0367632 --- /dev/null +++ b/templates/pages/forks.html @@ -0,0 +1,36 @@ +{{ template "header" .}} +{{ template "gist_header" .}} + {{ if ne (len .forks) 0 }} + + {{ else }} +
+ + + + +

No public forks

+
+ {{ end }} +{{ template "gist_footer" .}} +{{ template "footer" .}}