Ryanhub - file viewer
filename: assistant/tools/gitlog.go
branch: main
back to repo
package tools

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"
	"time"

	"assistant/config"
)

const gitPushReportToolName = "get_git_push_report"

var gitPushReportParams = json.RawMessage(`{
  "type": "object",
  "properties": {
    "date": {
      "type": "string",
      "description": "UTC date in YYYY-MM-DD format. Defaults to today (UTC)."
    },
    "max_commits": {
      "type": "integer",
      "description": "Maximum focused-user commits to include (default 25, max 200).",
      "minimum": 1,
      "maximum": 200
    }
  },
  "additionalProperties": false
}`)

type gitPushReportArgs struct {
	Date       string `json:"date"`
	MaxCommits int    `json:"max_commits"`
}

type gitPushEntry struct {
	TS      time.Time
	Repo    string
	User    string
	Ref     string
	Commit  string
	Author  string
	Message string
}

func newGitLogTools(cfg config.GitLogToolConfig) []Tool {
	logURL := strings.TrimSpace(cfg.LogURL)
	focusUser := strings.ToLower(strings.TrimSpace(cfg.FocusUser))
	return []Tool{
		{
			Name:        gitPushReportToolName,
			Description: "Summarize git pushes for a given day, focusing on the configured main user and noting activity from other users.",
			Parameters:  gitPushReportParams,
			Run: func(ctx context.Context, args json.RawMessage) (string, error) {
				var a gitPushReportArgs
				if len(args) > 0 && string(args) != "null" {
					if err := json.Unmarshal(args, &a); err != nil {
						return "", err
					}
				}

				targetDate := time.Now().UTC().Format("2006-01-02")
				if s := strings.TrimSpace(a.Date); s != "" {
					if _, err := time.Parse("2006-01-02", s); err != nil {
						return "", fmt.Errorf("invalid date %q (expected YYYY-MM-DD)", s)
					}
					targetDate = s
				}
				maxCommits := a.MaxCommits
				if maxCommits <= 0 {
					maxCommits = 25
				}
				if maxCommits > 200 {
					maxCommits = 200
				}

				entries, err := fetchGitPushEntries(ctx, logURL)
				if err != nil {
					return "", err
				}
				return summarizeGitPushes(entries, targetDate, focusUser, maxCommits), nil
			},
		},
	}
}

func fetchGitPushEntries(ctx context.Context, logURL string) ([]gitPushEntry, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, logURL, nil)
	if err != nil {
		return nil, err
	}
	client := &http.Client{Timeout: 20 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		b, _ := io.ReadAll(io.LimitReader(res.Body, 512))
		return nil, fmt.Errorf("gitlog fetch failed: %s: %s", res.Status, strings.TrimSpace(string(b)))
	}

	sc := bufio.NewScanner(io.LimitReader(res.Body, 8<<20))
	sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	out := make([]gitPushEntry, 0, 256)
	for sc.Scan() {
		line := strings.TrimSpace(sc.Text())
		if line == "" {
			continue
		}
		entry, ok := parseGitPushLine(line)
		if !ok {
			continue
		}
		out = append(out, entry)
	}
	if err := sc.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func parseGitPushLine(line string) (gitPushEntry, bool) {
	parts := strings.Split(line, "|")
	if len(parts) < 6 {
		return gitPushEntry{}, false
	}
	ts, err := time.Parse(time.RFC3339, strings.TrimSpace(parts[0]))
	if err != nil {
		return gitPushEntry{}, false
	}
	entry := gitPushEntry{TS: ts}
	for i := 1; i < len(parts); i++ {
		p := strings.TrimSpace(parts[i])
		if p == "" {
			continue
		}
		kv := strings.SplitN(p, "=", 2)
		if len(kv) != 2 {
			continue
		}
		k := strings.TrimSpace(kv[0])
		v := strings.TrimSpace(kv[1])
		switch k {
		case "repo":
			entry.Repo = v
		case "user":
			entry.User = v
		case "ref":
			entry.Ref = v
		case "commit":
			entry.Commit = v
		case "author":
			entry.Author = v
		case "message":
			entry.Message = v
		}
	}
	if entry.Repo == "" || entry.User == "" || entry.Commit == "" {
		return gitPushEntry{}, false
	}
	return entry, true
}

func summarizeGitPushes(entries []gitPushEntry, targetDate string, focusUser string, maxCommits int) string {
	filtered := make([]gitPushEntry, 0, len(entries))
	for _, e := range entries {
		if e.TS.UTC().Format("2006-01-02") != targetDate {
			continue
		}
		filtered = append(filtered, e)
	}
	sort.Slice(filtered, func(i, j int) bool {
		return filtered[i].TS.Before(filtered[j].TS)
	})

	if len(filtered) == 0 {
		return fmt.Sprintf("No git push entries found for %s.", targetDate)
	}

	focus := make([]gitPushEntry, 0, len(filtered))
	otherByUser := map[string]int{}
	byRepo := map[string]int{}
	seenFocusCommits := map[string]struct{}{}
	for _, e := range filtered {
		byRepo[e.Repo]++
		if strings.EqualFold(e.User, focusUser) {
			if _, ok := seenFocusCommits[e.Commit]; ok {
				continue
			}
			seenFocusCommits[e.Commit] = struct{}{}
			focus = append(focus, e)
		} else {
			otherByUser[e.User]++
		}
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Git push report for %s (UTC)\n", targetDate))
	sb.WriteString(fmt.Sprintf("Focus user: %s\n", focusUser))
	sb.WriteString(fmt.Sprintf("Total push log entries: %d\n", len(filtered)))
	sb.WriteString(fmt.Sprintf("Unique commits by %s: %d\n", focusUser, len(focus)))

	repos := make([]string, 0, len(byRepo))
	for r := range byRepo {
		repos = append(repos, r)
	}
	sort.Strings(repos)
	sb.WriteString("Repos touched:\n")
	for _, r := range repos {
		sb.WriteString(fmt.Sprintf("- %s (%d entries)\n", r, byRepo[r]))
	}

	if len(otherByUser) > 0 {
		users := make([]string, 0, len(otherByUser))
		for u := range otherByUser {
			users = append(users, u)
		}
		sort.Strings(users)
		sb.WriteString("Other users activity (cute note):\n")
		for _, u := range users {
			sb.WriteString(fmt.Sprintf("- %s: %d entries\n", u, otherByUser[u]))
		}
	} else {
		sb.WriteString("Other users activity (cute note): none today.\n")
	}

	if len(focus) == 0 {
		sb.WriteString(fmt.Sprintf("No commits found for focus user %s on this date.", focusUser))
		return sb.String()
	}

	sb.WriteString(fmt.Sprintf("Commits by %s:\n", focusUser))
	limit := len(focus)
	if limit > maxCommits {
		limit = maxCommits
	}
	for i := 0; i < limit; i++ {
		e := focus[i]
		ref := e.Ref
		if ref == "" {
			ref = "-"
		}
		sb.WriteString(fmt.Sprintf(
			"- %s | %s | %s | %s\n",
			e.TS.UTC().Format(time.RFC3339),
			e.Repo,
			ref,
			e.Message,
		))
	}
	if len(focus) > limit {
		sb.WriteString(fmt.Sprintf("... and %d more commits by %s.\n", len(focus)-limit, focusUser))
	}
	return sb.String()
}