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()
}