Add git implementation

Change-Id: I3bb5986fe244b310038b7b2ec4359d8439a158de
diff --git a/server/git/git.go b/server/git/git.go
new file mode 100644
index 0000000..bf95f7c
--- /dev/null
+++ b/server/git/git.go
@@ -0,0 +1,788 @@
+package git
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// GitInterface defines the contract for Git operations
+type GitInterface interface {
+	// Repository operations
+	Init(ctx context.Context, path string) error
+	Clone(ctx context.Context, url, path string) error
+	IsRepository(ctx context.Context, path string) (bool, error)
+
+	// Status and information
+	Status(ctx context.Context) (*Status, error)
+	Log(ctx context.Context, options LogOptions) ([]Commit, error)
+	Show(ctx context.Context, ref string) (*Commit, error)
+
+	// Branch operations
+	ListBranches(ctx context.Context) ([]Branch, error)
+	CreateBranch(ctx context.Context, name string, startPoint string) error
+	Checkout(ctx context.Context, ref string) error
+	DeleteBranch(ctx context.Context, name string, force bool) error
+	GetCurrentBranch(ctx context.Context) (string, error)
+
+	// Commit operations
+	Add(ctx context.Context, paths []string) error
+	AddAll(ctx context.Context) error
+	Commit(ctx context.Context, message string, options CommitOptions) error
+
+	// Remote operations
+	ListRemotes(ctx context.Context) ([]Remote, error)
+	AddRemote(ctx context.Context, name, url string) error
+	RemoveRemote(ctx context.Context, name string) error
+	Fetch(ctx context.Context, remote string, options FetchOptions) error
+	Pull(ctx context.Context, remote, branch string) error
+	Push(ctx context.Context, remote, branch string, options PushOptions) error
+
+	// Merge operations
+	Merge(ctx context.Context, ref string, options MergeOptions) error
+	MergeBase(ctx context.Context, ref1, ref2 string) (string, error)
+
+	// Configuration
+	GetConfig(ctx context.Context, key string) (string, error)
+	SetConfig(ctx context.Context, key, value string) error
+	GetUserConfig(ctx context.Context) (*UserConfig, error)
+	SetUserConfig(ctx context.Context, config UserConfig) error
+}
+
+// Status represents the current state of the repository
+type Status struct {
+	Branch    string
+	IsClean   bool
+	Staged    []FileStatus
+	Unstaged  []FileStatus
+	Untracked []string
+	Conflicts []string
+}
+
+// FileStatus represents the status of a file
+type FileStatus struct {
+	Path   string
+	Status string // "modified", "added", "deleted", "renamed", etc.
+	Staged bool
+}
+
+// Commit represents a Git commit
+type Commit struct {
+	Hash      string
+	Author    Author
+	Committer Author
+	Message   string
+	Parents   []string
+	Timestamp time.Time
+	Files     []CommitFile
+}
+
+// Author represents a Git author or committer
+type Author struct {
+	Name  string
+	Email string
+	Time  time.Time
+}
+
+// CommitFile represents a file in a commit
+type CommitFile struct {
+	Path      string
+	Status    string // "added", "modified", "deleted", "renamed"
+	Additions int
+	Deletions int
+}
+
+// Branch represents a Git branch
+type Branch struct {
+	Name      string
+	IsCurrent bool
+	IsRemote  bool
+	Commit    string
+	Message   string
+}
+
+// Remote represents a Git remote
+type Remote struct {
+	Name string
+	URL  string
+}
+
+// UserConfig represents Git user configuration
+type UserConfig struct {
+	Name  string
+	Email string
+}
+
+// LogOptions defines options for log operations
+type LogOptions struct {
+	MaxCount int
+	Since    time.Time
+	Until    time.Time
+	Author   string
+	Path     string
+	Oneline  bool
+}
+
+// CommitOptions defines options for commit operations
+type CommitOptions struct {
+	Author     *Author
+	Committer  *Author
+	Sign       bool
+	AllowEmpty bool
+}
+
+// FetchOptions defines options for fetch operations
+type FetchOptions struct {
+	All   bool
+	Tags  bool
+	Depth int
+	Prune bool
+}
+
+// PushOptions defines options for push operations
+type PushOptions struct {
+	Force       bool
+	Tags        bool
+	SetUpstream bool
+}
+
+// MergeOptions defines options for merge operations
+type MergeOptions struct {
+	NoFF     bool
+	Message  string
+	Strategy string
+}
+
+// GitError represents a Git-specific error
+type GitError struct {
+	Command string
+	Output  string
+	Err     error
+}
+
+func (e *GitError) Error() string {
+	if e.Err != nil {
+		return fmt.Sprintf("git %s failed: %v\nOutput: %s", e.Command, e.Err, e.Output)
+	}
+	return fmt.Sprintf("git %s failed\nOutput: %s", e.Command, e.Output)
+}
+
+func (e *GitError) Unwrap() error {
+	return e.Err
+}
+
+// Git implementation using os/exec to call git commands
+type Git struct {
+	repoPath string
+	config   GitConfig
+}
+
+// GitConfig holds configuration for Git operations
+type GitConfig struct {
+	Timeout time.Duration
+	Env     map[string]string
+}
+
+// NewGit creates a new Git instance
+func NewGit(repoPath string, config GitConfig) GitInterface {
+	if config.Timeout == 0 {
+		config.Timeout = 30 * time.Second
+	}
+
+	return &Git{
+		repoPath: repoPath,
+		config:   config,
+	}
+}
+
+// DefaultGit creates a Git instance with default configuration
+func DefaultGit(repoPath string) GitInterface {
+	return NewGit(repoPath, GitConfig{
+		Timeout: 30 * time.Second,
+		Env:     make(map[string]string),
+	})
+}
+
+// Ensure Git implements GitInterface
+var _ GitInterface = (*Git)(nil)
+
+// Init initializes a new Git repository
+func (g *Git) Init(ctx context.Context, path string) error {
+	cmd := exec.CommandContext(ctx, "git", "init")
+	cmd.Dir = path
+	return g.runCommand(cmd, "init")
+}
+
+// Clone clones a repository from URL to path
+func (g *Git) Clone(ctx context.Context, url, path string) error {
+	cmd := exec.CommandContext(ctx, "git", "clone", url, path)
+	return g.runCommand(cmd, "clone")
+}
+
+// IsRepository checks if a path is a valid Git repository
+func (g *Git) IsRepository(ctx context.Context, path string) (bool, error) {
+	return g.isValidRepo(path), nil
+}
+
+// Status returns the current status of the repository
+func (g *Git) Status(ctx context.Context) (*Status, error) {
+	cmd := exec.CommandContext(ctx, "git", "status", "--porcelain", "--branch")
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "status")
+	if err != nil {
+		return nil, err
+	}
+
+	return g.parseStatus(output)
+}
+
+// Log returns commit history
+func (g *Git) Log(ctx context.Context, options LogOptions) ([]Commit, error) {
+	args := []string{"log", "--format=%H%n%an%n%ae%n%at%n%s%n%cn%n%ce%n%ct%n%P"}
+
+	if options.MaxCount > 0 {
+		args = append(args, fmt.Sprintf("-%d", options.MaxCount))
+	}
+
+	if !options.Since.IsZero() {
+		args = append(args, fmt.Sprintf("--since=%s", options.Since.Format(time.RFC3339)))
+	}
+
+	if !options.Until.IsZero() {
+		args = append(args, fmt.Sprintf("--until=%s", options.Until.Format(time.RFC3339)))
+	}
+
+	if options.Author != "" {
+		args = append(args, fmt.Sprintf("--author=%s", options.Author))
+	}
+
+	if options.Path != "" {
+		args = append(args, "--", options.Path)
+	}
+
+	if options.Oneline {
+		args = append(args, "--oneline")
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "log")
+	if err != nil {
+		return nil, err
+	}
+
+	return g.parseLog(output)
+}
+
+// Show shows commit details
+func (g *Git) Show(ctx context.Context, ref string) (*Commit, error) {
+	cmd := exec.CommandContext(ctx, "git", "show", "--format=json", ref)
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "show")
+	if err != nil {
+		return nil, err
+	}
+
+	commits, err := g.parseLog(output)
+	if err != nil || len(commits) == 0 {
+		return nil, &GitError{Command: "show", Output: "failed to parse commit"}
+	}
+
+	return &commits[0], nil
+}
+
+// ListBranches returns all branches
+func (g *Git) ListBranches(ctx context.Context) ([]Branch, error) {
+	cmd := exec.CommandContext(ctx, "git", "branch", "-a", "--format=%(refname:short)%09%(objectname)%09%(contents:subject)")
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "branch")
+	if err != nil {
+		return nil, err
+	}
+
+	return g.parseBranches(output)
+}
+
+// CreateBranch creates a new branch
+func (g *Git) CreateBranch(ctx context.Context, name string, startPoint string) error {
+	args := []string{"branch", name}
+	if startPoint != "" {
+		args = append(args, startPoint)
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "branch")
+}
+
+// Checkout switches to a branch or commit
+func (g *Git) Checkout(ctx context.Context, ref string) error {
+	cmd := exec.CommandContext(ctx, "git", "checkout", ref)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "checkout")
+}
+
+// DeleteBranch deletes a branch
+func (g *Git) DeleteBranch(ctx context.Context, name string, force bool) error {
+	args := []string{"branch"}
+	if force {
+		args = append(args, "-D")
+	} else {
+		args = append(args, "-d")
+	}
+	args = append(args, name)
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "branch")
+}
+
+// GetCurrentBranch returns the current branch name
+func (g *Git) GetCurrentBranch(ctx context.Context) (string, error) {
+	cmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD")
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "rev-parse")
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(output), nil
+}
+
+// Add stages files for commit
+func (g *Git) Add(ctx context.Context, paths []string) error {
+	args := append([]string{"add"}, paths...)
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "add")
+}
+
+// AddAll stages all changes
+func (g *Git) AddAll(ctx context.Context) error {
+	cmd := exec.CommandContext(ctx, "git", "add", "-A")
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "add")
+}
+
+// Commit creates a new commit
+func (g *Git) Commit(ctx context.Context, message string, options CommitOptions) error {
+	args := []string{"commit", "-m", message}
+
+	if options.Author != nil {
+		args = append(args, fmt.Sprintf("--author=%s <%s>", options.Author.Name, options.Author.Email))
+	}
+
+	if options.Sign {
+		args = append(args, "-S")
+	}
+
+	if options.AllowEmpty {
+		args = append(args, "--allow-empty")
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "commit")
+}
+
+// ListRemotes returns all remotes
+func (g *Git) ListRemotes(ctx context.Context) ([]Remote, error) {
+	cmd := exec.CommandContext(ctx, "git", "remote", "-v")
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "remote")
+	if err != nil {
+		return nil, err
+	}
+
+	return g.parseRemotes(output)
+}
+
+// AddRemote adds a new remote
+func (g *Git) AddRemote(ctx context.Context, name, url string) error {
+	cmd := exec.CommandContext(ctx, "git", "remote", "add", name, url)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "remote")
+}
+
+// RemoveRemote removes a remote
+func (g *Git) RemoveRemote(ctx context.Context, name string) error {
+	cmd := exec.CommandContext(ctx, "git", "remote", "remove", name)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "remote")
+}
+
+// Fetch fetches from a remote
+func (g *Git) Fetch(ctx context.Context, remote string, options FetchOptions) error {
+	args := []string{"fetch"}
+
+	if options.All {
+		args = append(args, "--all")
+	} else if remote != "" {
+		args = append(args, remote)
+	}
+
+	if options.Tags {
+		args = append(args, "--tags")
+	}
+
+	if options.Depth > 0 {
+		args = append(args, fmt.Sprintf("--depth=%d", options.Depth))
+	}
+
+	if options.Prune {
+		args = append(args, "--prune")
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "fetch")
+}
+
+// Pull pulls from a remote
+func (g *Git) Pull(ctx context.Context, remote, branch string) error {
+	args := []string{"pull"}
+	if remote != "" {
+		args = append(args, remote)
+		if branch != "" {
+			args = append(args, branch)
+		}
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "pull")
+}
+
+// Push pushes to a remote
+func (g *Git) Push(ctx context.Context, remote, branch string, options PushOptions) error {
+	args := []string{"push"}
+
+	if options.Force {
+		args = append(args, "--force")
+	}
+
+	if options.Tags {
+		args = append(args, "--tags")
+	}
+
+	if options.SetUpstream {
+		args = append(args, "--set-upstream")
+	}
+
+	if remote != "" {
+		args = append(args, remote)
+		if branch != "" {
+			args = append(args, branch)
+		}
+	}
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "push")
+}
+
+// Merge merges a branch into current branch
+func (g *Git) Merge(ctx context.Context, ref string, options MergeOptions) error {
+	args := []string{"merge"}
+
+	if options.NoFF {
+		args = append(args, "--no-ff")
+	}
+
+	if options.Message != "" {
+		args = append(args, "-m", options.Message)
+	}
+
+	if options.Strategy != "" {
+		args = append(args, "-s", options.Strategy)
+	}
+
+	args = append(args, ref)
+
+	cmd := exec.CommandContext(ctx, "git", args...)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "merge")
+}
+
+// MergeBase finds the common ancestor of two commits
+func (g *Git) MergeBase(ctx context.Context, ref1, ref2 string) (string, error) {
+	cmd := exec.CommandContext(ctx, "git", "merge-base", ref1, ref2)
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "merge-base")
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(output), nil
+}
+
+// GetConfig gets a Git configuration value
+func (g *Git) GetConfig(ctx context.Context, key string) (string, error) {
+	cmd := exec.CommandContext(ctx, "git", "config", "--get", key)
+	cmd.Dir = g.repoPath
+	output, err := g.runCommandWithOutput(cmd, "config")
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(output), nil
+}
+
+// SetConfig sets a Git configuration value
+func (g *Git) SetConfig(ctx context.Context, key, value string) error {
+	cmd := exec.CommandContext(ctx, "git", "config", key, value)
+	cmd.Dir = g.repoPath
+	return g.runCommand(cmd, "config")
+}
+
+// GetUserConfig gets user configuration
+func (g *Git) GetUserConfig(ctx context.Context) (*UserConfig, error) {
+	name, err := g.GetConfig(ctx, "user.name")
+	if err != nil {
+		return nil, err
+	}
+
+	email, err := g.GetConfig(ctx, "user.email")
+	if err != nil {
+		return nil, err
+	}
+
+	return &UserConfig{
+		Name:  name,
+		Email: email,
+	}, nil
+}
+
+// SetUserConfig sets user configuration
+func (g *Git) SetUserConfig(ctx context.Context, config UserConfig) error {
+	if err := g.SetConfig(ctx, "user.name", config.Name); err != nil {
+		return err
+	}
+
+	return g.SetConfig(ctx, "user.email", config.Email)
+}
+
+// Helper methods
+
+func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return &GitError{
+			Command: command,
+			Output:  string(output),
+			Err:     err,
+		}
+	}
+	return nil
+}
+
+func (g *Git) runCommandWithOutput(cmd *exec.Cmd, command string) (string, error) {
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", &GitError{
+			Command: command,
+			Output:  string(output),
+			Err:     err,
+		}
+	}
+	return string(output), nil
+}
+
+func (g *Git) isValidRepo(path string) bool {
+	gitDir := filepath.Join(path, ".git")
+	info, err := os.Stat(gitDir)
+	return err == nil && info.IsDir()
+}
+
+func (g *Git) parseStatus(output string) (*Status, error) {
+	lines := strings.Split(strings.TrimSpace(output), "\n")
+	status := &Status{
+		Staged:    []FileStatus{},
+		Unstaged:  []FileStatus{},
+		Untracked: []string{},
+		Conflicts: []string{},
+	}
+
+	for _, line := range lines {
+		if strings.HasPrefix(line, "## ") {
+			// Parse branch info
+			parts := strings.Fields(line[3:])
+			if len(parts) > 0 {
+				status.Branch = parts[0]
+			}
+			continue
+		}
+
+		if len(line) < 3 {
+			continue
+		}
+
+		// Parse file status
+		staged := line[0:1]
+		unstaged := line[1:2]
+		path := strings.TrimSpace(line[3:])
+
+		if staged != " " {
+			status.Staged = append(status.Staged, FileStatus{
+				Path:   path,
+				Status: g.parseStatusCode(staged),
+				Staged: true,
+			})
+		}
+
+		if unstaged != " " {
+			status.Unstaged = append(status.Unstaged, FileStatus{
+				Path:   path,
+				Status: g.parseStatusCode(unstaged),
+				Staged: false,
+			})
+		}
+
+		if staged == " " && unstaged == "?" {
+			status.Untracked = append(status.Untracked, path)
+		}
+	}
+
+	status.IsClean = len(status.Staged) == 0 && len(status.Unstaged) == 0 && len(status.Untracked) == 0
+	return status, nil
+}
+
+func (g *Git) parseStatusCode(code string) string {
+	switch code {
+	case "M":
+		return "modified"
+	case "A":
+		return "added"
+	case "D":
+		return "deleted"
+	case "R":
+		return "renamed"
+	case "C":
+		return "copied"
+	case "U":
+		return "unmerged"
+	default:
+		return "unknown"
+	}
+}
+
+func (g *Git) parseLog(output string) ([]Commit, error) {
+	lines := strings.Split(strings.TrimSpace(output), "\n")
+	var commits []Commit
+
+	// Each commit takes 9 lines in the format:
+	// Hash, AuthorName, AuthorEmail, AuthorTime, Subject, CommitterName, CommitterEmail, CommitterTime, Parents
+	for i := 0; i < len(lines); i += 9 {
+		if i+8 >= len(lines) {
+			break
+		}
+
+		hash := lines[i]
+		authorName := lines[i+1]
+		authorEmail := lines[i+2]
+		authorTimeStr := lines[i+3]
+		subject := lines[i+4]
+		committerName := lines[i+5]
+		committerEmail := lines[i+6]
+		committerTimeStr := lines[i+7]
+		parentsStr := lines[i+8]
+
+		// Parse timestamps
+		authorTime, _ := strconv.ParseInt(authorTimeStr, 10, 64)
+		committerTime, _ := strconv.ParseInt(committerTimeStr, 10, 64)
+
+		// Parse parents
+		var parents []string
+		if parentsStr != "" {
+			parents = strings.Fields(parentsStr)
+		}
+
+		commit := Commit{
+			Hash: hash,
+			Author: Author{
+				Name:  authorName,
+				Email: authorEmail,
+				Time:  time.Unix(authorTime, 0),
+			},
+			Committer: Author{
+				Name:  committerName,
+				Email: committerEmail,
+				Time:  time.Unix(committerTime, 0),
+			},
+			Message:   subject,
+			Parents:   parents,
+			Timestamp: time.Unix(authorTime, 0),
+		}
+
+		commits = append(commits, commit)
+	}
+
+	return commits, nil
+}
+
+func (g *Git) parseBranches(output string) ([]Branch, error) {
+	lines := strings.Split(strings.TrimSpace(output), "\n")
+	var branches []Branch
+
+	for _, line := range lines {
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+
+		parts := strings.Split(line, "\t")
+		if len(parts) < 3 {
+			continue
+		}
+
+		branch := Branch{
+			Name:     parts[0],
+			Commit:   parts[1],
+			Message:  parts[2],
+			IsRemote: strings.HasPrefix(parts[0], "remotes/"),
+		}
+
+		// Check if this is the current branch
+		if !branch.IsRemote {
+			branch.IsCurrent = strings.HasPrefix(line, "*")
+		}
+
+		branches = append(branches, branch)
+	}
+
+	return branches, nil
+}
+
+func (g *Git) parseRemotes(output string) ([]Remote, error) {
+	lines := strings.Split(strings.TrimSpace(output), "\n")
+	var remotes []Remote
+	seen := make(map[string]bool)
+
+	for _, line := range lines {
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+
+		parts := strings.Fields(line)
+		if len(parts) < 3 {
+			continue
+		}
+
+		name := parts[0]
+		if seen[name] {
+			continue
+		}
+
+		remotes = append(remotes, Remote{
+			Name: name,
+			URL:  parts[1],
+		})
+		seen[name] = true
+	}
+
+	return remotes, nil
+}