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 }