Initial git sorage backend implementation

This commit is contained in:
2024-09-28 14:52:02 +02:00
parent 122860d230
commit 0558ea292b
8 changed files with 427 additions and 26 deletions

View File

@@ -73,11 +73,6 @@ func DeleteFile(fs *filesystem.FileSystem) http.HandlerFunc {
}
}
var defaultSettings = models.UserSettings{
Theme: "light",
AutoSave: false,
}
func GetSettings(db *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userIDStr := r.URL.Query().Get("userId")
@@ -93,7 +88,7 @@ func GetSettings(db *db.DB) http.HandlerFunc {
return
}
settings.SetDefaults(defaultSettings)
settings.SetDefaults()
json.NewEncoder(w).Encode(settings)
}
@@ -107,7 +102,7 @@ func UpdateSettings(db *db.DB) http.HandlerFunc {
return
}
settings.SetDefaults(defaultSettings)
settings.SetDefaults()
if err := settings.Validate(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -132,3 +127,44 @@ func UpdateSettings(db *db.DB) http.HandlerFunc {
json.NewEncoder(w).Encode(savedSettings)
}
}
func StageCommitAndPush(fs *filesystem.FileSystem) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody struct {
Message string `json:"message"`
}
err := json.NewDecoder(r.Body).Decode(&requestBody)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if requestBody.Message == "" {
http.Error(w, "Commit message is required", http.StatusBadRequest)
return
}
err = fs.StageCommitAndPush(requestBody.Message)
if err != nil {
http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Changes staged, committed, and pushed successfully"))
}
}
func PullChanges(fs *filesystem.FileSystem) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := fs.Pull()
if err != nil {
http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Pulled changes from remote"})
}
}

View File

@@ -19,5 +19,9 @@ func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem) {
r.Post("/*", SaveFile(fs))
r.Delete("/*", DeleteFile(fs))
})
r.Route("/git", func(r chi.Router) {
r.Post("commit", StageCommitAndPush(fs))
r.Post("/pull", PullChanges(fs))
})
})
}

View File

@@ -2,13 +2,19 @@ package filesystem
import (
"errors"
"novamd/internal/gitutils"
"novamd/internal/models"
"os"
"path/filepath"
"strings"
git "novamd/internal/gitutils"
)
type FileSystem struct {
RootDir string
RootDir string
GitRepo *gitutils.GitRepo
Settings *models.Settings
}
type FileNode struct {
@@ -17,8 +23,30 @@ type FileNode struct {
Files []FileNode `json:"files,omitempty"`
}
func New(rootDir string) *FileSystem {
return &FileSystem{RootDir: rootDir}
func New(rootDir string, settings *models.Settings) *FileSystem {
fs := &FileSystem{
RootDir: rootDir,
Settings: settings,
}
if settings.Settings.GitEnabled {
fs.GitRepo = git.New(
settings.Settings.GitURL,
settings.Settings.GitUser,
settings.Settings.GitToken,
rootDir,
)
}
return fs
}
func (fs *FileSystem) InitializeGitRepo() error {
if fs.GitRepo == nil {
return errors.New("git settings not configured")
}
return fs.GitRepo.EnsureRepo()
}
// validatePath checks if the given path is within the root directory
@@ -96,6 +124,11 @@ func (fs *FileSystem) SaveFile(filePath string, content []byte) error {
return err
}
if fs.Settings.Settings.GitAutoCommit && fs.GitRepo != nil {
message := strings.Replace(fs.Settings.Settings.GitCommitMsgTemplate, "${filename}", filePath, -1)
return fs.StageCommitAndPush(message)
}
return os.WriteFile(fullPath, content, 0644)
}
@@ -106,3 +139,23 @@ func (fs *FileSystem) DeleteFile(filePath string) error {
}
return os.Remove(fullPath)
}
func (fs *FileSystem) StageCommitAndPush(message string) error {
if fs.GitRepo == nil {
return errors.New("git settings not configured")
}
if err := fs.GitRepo.Commit(message); err != nil {
return err
}
return fs.GitRepo.Push()
}
func (fs *FileSystem) Pull() error {
if fs.GitRepo == nil {
return errors.New("git settings not configured")
}
return fs.GitRepo.Pull()
}

View File

@@ -0,0 +1,133 @@
package gitutils
import (
"fmt"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
type GitRepo struct {
URL string
Username string
Token string
WorkDir string
repo *git.Repository
}
func New(url, username, token, workDir string) *GitRepo {
return &GitRepo{
URL: url,
Username: username,
Token: token,
WorkDir: workDir,
}
}
func (g *GitRepo) Clone() error {
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
var err error
g.repo, err = git.PlainClone(g.WorkDir, false, &git.CloneOptions{
URL: g.URL,
Auth: auth,
Progress: os.Stdout,
})
if err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
return nil
}
func (g *GitRepo) Pull() error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := g.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
err = w.Pull(&git.PullOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to pull changes: %w", err)
}
return nil
}
func (g *GitRepo) Commit(message string) error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
w, err := g.repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
_, err = w.Add(".")
if err != nil {
return fmt.Errorf("failed to add changes: %w", err)
}
_, err = w.Commit(message, &git.CommitOptions{})
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}
func (g *GitRepo) Push() error {
if g.repo == nil {
return fmt.Errorf("repository not initialized")
}
auth := &http.BasicAuth{
Username: g.Username,
Password: g.Token,
}
err := g.repo.Push(&git.PushOptions{
Auth: auth,
Progress: os.Stdout,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to push changes: %w", err)
}
return nil
}
func (g *GitRepo) EnsureRepo() error {
if _, err := os.Stat(filepath.Join(g.WorkDir, ".git")); os.IsNotExist(err) {
return g.Clone()
}
var err error
g.repo, err = git.PlainOpen(g.WorkDir)
if err != nil {
return fmt.Errorf("failed to open existing repository: %w", err)
}
return g.Pull()
}

View File

@@ -7,8 +7,14 @@ import (
)
type UserSettings struct {
Theme string `json:"theme" validate:"oneof=light dark"`
AutoSave bool `json:"autoSave"`
Theme string `json:"theme" validate:"oneof=light dark"`
AutoSave bool `json:"autoSave"`
GitEnabled bool `json:"git_enabled"`
GitURL string `json:"git_url" validate:"required_with=GitEnabled"`
GitUser string `json:"git_user" validate:"required_with=GitEnabled"`
GitToken string `json:"git_token" validate:"required_with=GitEnabled"`
GitAutoCommit bool `json:"git_auto_commit"`
GitCommitMsgTemplate string `json:"git_commit_msg_template"`
}
type Settings struct {
@@ -16,15 +22,25 @@ type Settings struct {
Settings UserSettings `json:"settings" validate:"required"`
}
var defaultUserSettings = UserSettings{
Theme: "light",
AutoSave: false,
GitEnabled: false,
GitCommitMsgTemplate: "Update ${filename}",
}
var validate = validator.New()
func (s *Settings) Validate() error {
return validate.Struct(s)
}
func (s *Settings) SetDefaults(defaults UserSettings) {
func (s *Settings) SetDefaults() {
if s.Settings.Theme == "" {
s.Settings.Theme = defaults.Theme
s.Settings.Theme = defaultUserSettings.Theme
}
if s.Settings.GitCommitMsgTemplate == "" {
s.Settings.GitCommitMsgTemplate = defaultUserSettings.GitCommitMsgTemplate
}
}