Phase 2-6: Git sync, sharing, 2FA, AI integration
- 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
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user