4df87cbf9a
- Git: init, commit, log, diff, restore, remotes, push/pull - Auto-commit on every file save - Sharing: share/unshare files with other users (ro/rw) - Shared documents view in sidebar - 2FA: TOTP setup/verify/disable, enforced at login - AI: verify spec endpoint (LiteLLM), generate (summarize/prompt/expand) - Light/dark theme with CSS variables - File delete (recursive for folders) - Admin panel + preferences panel - File creation timestamp display
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
package git
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// InitRepo initializes a git repo in the user's file directory if not already initialized.
|
|
func InitRepo(dataDir, userID string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
gitDir := filepath.Join(repoDir, ".git")
|
|
if _, err := os.Stat(gitDir); err == nil {
|
|
return nil // already initialized
|
|
}
|
|
return run(repoDir, "git", "init")
|
|
}
|
|
|
|
// Commit stages all changes and commits with the given message.
|
|
func Commit(dataDir, userID, message string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
if err := run(repoDir, "git", "add", "-A"); err != nil {
|
|
return err
|
|
}
|
|
// Check if there's anything to commit
|
|
out, err := output(repoDir, "git", "status", "--porcelain")
|
|
if err != nil || strings.TrimSpace(out) == "" {
|
|
return nil // nothing to commit
|
|
}
|
|
return runEnv(repoDir, []string{
|
|
"GIT_AUTHOR_NAME=MarkdownHub",
|
|
"GIT_AUTHOR_EMAIL=user@markdownhub",
|
|
"GIT_COMMITTER_NAME=MarkdownHub",
|
|
"GIT_COMMITTER_EMAIL=user@markdownhub",
|
|
}, "git", "commit", "-m", message)
|
|
}
|
|
|
|
// AutoCommit commits with an auto-generated message including timestamp.
|
|
func AutoCommit(dataDir, userID, filename string) error {
|
|
msg := fmt.Sprintf("Update %s (%s)", filename, time.Now().Format("2006-01-02 15:04"))
|
|
return Commit(dataDir, userID, msg)
|
|
}
|
|
|
|
// Log returns recent commit history for a file.
|
|
func Log(dataDir, userID, relPath string, limit int) ([]CommitInfo, error) {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
args := []string{"log", "--format=%H|%ai|%s", fmt.Sprintf("-n%d", limit)}
|
|
if relPath != "" {
|
|
args = append(args, "--", relPath)
|
|
}
|
|
out, err := output(repoDir, "git", args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var commits []CommitInfo
|
|
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "|", 3)
|
|
if len(parts) == 3 {
|
|
commits = append(commits, CommitInfo{
|
|
Hash: parts[0],
|
|
Date: parts[1],
|
|
Message: parts[2],
|
|
})
|
|
}
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
// Diff returns the diff for a specific commit.
|
|
func Diff(dataDir, userID, commitHash string) (string, error) {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
return output(repoDir, "git", "show", "--stat", "--patch", commitHash)
|
|
}
|
|
|
|
// Restore restores a file to a specific commit version.
|
|
func Restore(dataDir, userID, relPath, commitHash string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
return run(repoDir, "git", "checkout", commitHash, "--", relPath)
|
|
}
|
|
|
|
// AddRemote adds a named remote to the user's repo.
|
|
func AddRemote(dataDir, userID, name, url string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
// Remove if exists, then add
|
|
run(repoDir, "git", "remote", "remove", name)
|
|
return run(repoDir, "git", "remote", "add", name, url)
|
|
}
|
|
|
|
// Push pushes to a named remote.
|
|
func Push(dataDir, userID, remoteName, branch string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
if branch == "" {
|
|
branch = "master"
|
|
}
|
|
return run(repoDir, "git", "push", remoteName, branch)
|
|
}
|
|
|
|
// Pull pulls from a named remote.
|
|
func Pull(dataDir, userID, remoteName, branch string) error {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
if branch == "" {
|
|
branch = "master"
|
|
}
|
|
return run(repoDir, "git", "pull", "--rebase", remoteName, branch)
|
|
}
|
|
|
|
// ListRemotes returns configured remotes.
|
|
func ListRemotes(dataDir, userID string) ([]RemoteInfo, error) {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
out, err := output(repoDir, "git", "remote", "-v")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
seen := map[string]bool{}
|
|
var remotes []RemoteInfo
|
|
for _, line := range strings.Split(out, "\n") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 && !seen[parts[0]] {
|
|
seen[parts[0]] = true
|
|
remotes = append(remotes, RemoteInfo{Name: parts[0], URL: parts[1]})
|
|
}
|
|
}
|
|
return remotes, nil
|
|
}
|
|
|
|
// Status returns dirty file count.
|
|
func Status(dataDir, userID string) (int, error) {
|
|
repoDir := filepath.Join(dataDir, "files", userID)
|
|
out, err := output(repoDir, "git", "status", "--porcelain")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return 0, nil
|
|
}
|
|
return len(strings.Split(strings.TrimSpace(out), "\n")), nil
|
|
}
|
|
|
|
type CommitInfo struct {
|
|
Hash string `json:"hash"`
|
|
Date string `json:"date"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type RemoteInfo struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func run(dir string, name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Dir = dir
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
return cmd.Run()
|
|
}
|
|
|
|
func runEnv(dir string, env []string, name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Dir = dir
|
|
cmd.Env = append(os.Environ(), env...)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func output(dir string, name string, args ...string) (string, error) {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Dir = dir
|
|
out, err := cmd.Output()
|
|
return string(out), err
|
|
}
|