← back to kso1204__gitday

Function bodies 25 total

All specs Real LLM only Function bodies
cmd.runExport function · go · L26-L77 (52 LOC)
cmd/export.go
func runExport(cmd *cobra.Command, args []string) error {
	period, _ := cmd.Flags().GetString("period")
	outputPath, _ := cmd.Flags().GetString("output")

	now := time.Now()
	var since time.Time

	switch period {
	case "week":
		weekday := int(now.Weekday())
		if weekday == 0 {
			weekday = 7
		}
		monday := now.AddDate(0, 0, -(weekday - 1))
		since = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location())
	default:
		since = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	}

	scanPaths := viper.GetStringSlice("scan_paths")
	excludes := viper.GetStringSlice("exclude")
	author := viper.GetString("author")

	repos, err := git.ScanRepos(scanPaths, excludes)
	if err != nil {
		return fmt.Errorf("레포 스캔 실패: %w", err)
	}

	results, err := git.CollectLogs(repos, since, now, author)
	if err != nil {
		return fmt.Errorf("커밋 로그 수집 실패: %w", err)
	}

	if len(results) == 0 {
		fmt.Println("내보낼 커밋이 없습니다.")
		return nil
	}

	md := output.ToMarkdown
cmd.runInit function · go · L55-L81 (27 LOC)
cmd/init_cmd.go
func runInit(cmd *cobra.Command, args []string) error {
	home, err := os.UserHomeDir()
	if err != nil {
		return fmt.Errorf("홈 디렉토리 확인 실패: %w", err)
	}

	configPath := filepath.Join(home, ".gitday.yaml")

	if _, err := os.Stat(configPath); err == nil {
		fmt.Printf("⚠ 이미 설정 파일이 존재합니다: %s\n", configPath)
		fmt.Print("덮어쓰시겠습니까? (y/N): ")
		var answer string
		fmt.Scanln(&answer)
		if answer != "y" && answer != "Y" {
			fmt.Println("취소되었습니다.")
			return nil
		}
	}

	if err := os.WriteFile(configPath, []byte(defaultConfig), 0600); err != nil {
		return fmt.Errorf("설정 파일 생성 실패: %w", err)
	}

	fmt.Printf("✓ 설정 파일 생성됨: %s\n", configPath)
	fmt.Println("  scan_paths를 수정하여 스캔할 디렉토리를 지정하세요.")
	return nil
}
cmd.init function · go · L29-L45 (17 LOC)
cmd/root.go
func init() {
	cobra.OnInitialize(initConfig)

	// 기본 동작 = today (순환 참조 방지를 위해 init에서 설정)
	rootCmd.RunE = func(cmd *cobra.Command, args []string) error {
		return runToday(cmd, args)
	}

	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "설정 파일 경로 (기본: ~/.gitday.yaml)")
	rootCmd.PersistentFlags().String("author", "", "Git 저자 필터")
	rootCmd.PersistentFlags().Bool("summary", false, "AI 요약 포함")
	rootCmd.PersistentFlags().Bool("compact", false, "간략 출력 모드")

	viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
	viper.BindPFlag("output.compact", rootCmd.PersistentFlags().Lookup("compact"))
	viper.BindPFlag("summary", rootCmd.PersistentFlags().Lookup("summary"))
}
cmd.runSend function · go · L27-L84 (58 LOC)
cmd/send.go
func runSend(cmd *cobra.Command, args []string) error {
	useSlack, _ := cmd.Flags().GetBool("slack")
	if !useSlack {
		return fmt.Errorf("전송 대상을 지정하세요 (예: --slack)")
	}

	webhookURL := viper.GetString("slack.webhook_url")
	if webhookURL == "" {
		return fmt.Errorf("Slack webhook URL이 설정되지 않았습니다. ~/.gitday.yaml에서 설정하세요")
	}

	period, _ := cmd.Flags().GetString("period")
	now := time.Now()
	var since time.Time

	switch period {
	case "week":
		weekday := int(now.Weekday())
		if weekday == 0 {
			weekday = 7
		}
		monday := now.AddDate(0, 0, -(weekday - 1))
		since = time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location())
	default:
		since = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	}

	scanPaths := viper.GetStringSlice("scan_paths")
	excludes := viper.GetStringSlice("exclude")
	author := viper.GetString("author")

	repos, err := git.ScanRepos(scanPaths, excludes)
	if err != nil {
		return fmt.Errorf("레포 스캔 실패: %w", err)
	}

	res
cmd.runReport function · go · L33-L78 (46 LOC)
cmd/today.go
func runReport(since, until time.Time, period string) error {
	scanPaths := viper.GetStringSlice("scan_paths")
	excludes := viper.GetStringSlice("exclude")
	author := viper.GetString("author")

	// 1. 레포 스캔
	repos, err := git.ScanRepos(scanPaths, excludes)
	if err != nil {
		return fmt.Errorf("레포 스캔 실패: %w", err)
	}

	if len(repos) == 0 {
		fmt.Println("스캔된 Git 레포가 없습니다. gitday init으로 scan_paths를 설정하세요.")
		return nil
	}

	// 2. 커밋 로그 수집
	results, err := git.CollectLogs(repos, since, until, author)
	if err != nil {
		return fmt.Errorf("커밋 로그 수집 실패: %w", err)
	}

	if len(results) == 0 {
		fmt.Printf("📭 %s ~ %s 기간에 커밋이 없습니다.\n",
			since.Format("2006-01-02"),
			until.Format("2006-01-02 15:04"))
		return nil
	}

	// 3. 터미널 출력
	compact := viper.GetBool("output.compact")
	output.PrintReport(results, since, until, compact)

	// 4. AI 요약 + 로그 저장 (--summary 플래그)
	summary := viper.GetBool("summary")
	if summary {
		summaryText := getSummary(results, since)
		if summaryText != "" {
			output.Pr
cmd.getSummary function · go · L80-L114 (35 LOC)
cmd/today.go
func getSummary(results []git.RepoResult, since time.Time) string {
	providerName := viper.GetString("ai.provider")
	apiKey := viper.GetString("ai.api_key")
	model := viper.GetString("ai.model")
	ollamaURL := viper.GetString("ai.ollama_url")

	if envKey := os.Getenv("GITDAY_API_KEY"); envKey != "" {
		apiKey = envKey
	} else if envKey := os.Getenv("ANTHROPIC_API_KEY"); envKey != "" && providerName == "claude" {
		apiKey = envKey
	} else if envKey := os.Getenv("OPENAI_API_KEY"); envKey != "" && providerName == "openai" {
		apiKey = envKey
	}

	provider, err := ai.NewProvider(providerName, apiKey, model, ollamaURL)
	if err != nil {
		fmt.Fprintf(os.Stderr, "\n⚠ AI 요약 실패: %v\n", err)
		return ""
	}

	prompt := ai.BuildPrompt(results, since.Format("2006-01-02"))

	fmt.Printf("\n📝 AI 요약 생성 중 (%s)...\n", provider.Name())

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	text, err := provider.Summarize(ctx, prompt)
	if err != nil {
		fmt.Fprintf(os.S
cmd.saveLog function · go · L116-L140 (25 LOC)
cmd/today.go
func saveLog(results []git.RepoResult, since, until time.Time, period, summaryText string) {
	home, err := os.UserHomeDir()
	if err != nil {
		return
	}

	logDir := filepath.Join(home, ".gitday", "logs")
	if err := os.MkdirAll(logDir, 0755); err != nil {
		return
	}

	filename := since.Format("2006-01-02") + ".md"
	if period == "week" {
		filename = since.Format("2006-01-02") + "_week.md"
	}
	logPath := filepath.Join(logDir, filename)

	md := output.ToMarkdown(results, since, until, summaryText)
	if err := os.WriteFile(logPath, []byte(md), 0600); err != nil {
		fmt.Fprintf(os.Stderr, "⚠ 로그 저장 실패: %v\n", err)
		return
	}

	fmt.Printf("\n✓ 저장됨: %s\n", logPath)
}
Want this analysis on your repo? https://repobility.com/scan/
cmd.runWeek function · go · L19-L31 (13 LOC)
cmd/week.go
func runWeek(cmd *cobra.Command, args []string) error {
	now := time.Now()

	// 이번 주 월요일 00:00
	weekday := int(now.Weekday())
	if weekday == 0 {
		weekday = 7 // 일요일
	}
	monday := now.AddDate(0, 0, -(weekday - 1))
	since := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, now.Location())

	return runReport(since, now, "week")
}
ai.NewClaude function · go · L17-L22 (6 LOC)
internal/ai/claude.go
func NewClaude(apiKey, model string) *Claude {
	if model == "" {
		model = "claude-haiku-4-5-20251001"
	}
	return &Claude{apiKey: apiKey, model: model}
}
ai.Ollama.Summarize method · go · L30-L83 (54 LOC)
internal/ai/ollama.go
func (o *Ollama) Summarize(ctx context.Context, prompt string) (string, error) {
	body := map[string]any{
		"model": o.model,
		"messages": []map[string]string{
			{"role": "user", "content": prompt},
		},
		"stream": false,
	}

	jsonBody, err := json.Marshal(body)
	if err != nil {
		return "", err
	}

	url := o.baseURL + "/v1/chat/completions"
	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody))
	if err != nil {
		return "", err
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("Ollama 연결 실패 (%s): %w", o.baseURL, err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	if resp.StatusCode != 200 {
		return "", fmt.Errorf("Ollama API 에러 (%d): %s", resp.StatusCode, string(respBody))
	}

	var result struct {
		Choices []struct {
			Message struct {
				Content string `json:"content"`
			} `json:"message"`
		} `json
ai.NewOpenAI function · go · L17-L22 (6 LOC)
internal/ai/openai.go
func NewOpenAI(apiKey, model string) *OpenAI {
	if model == "" {
		model = "gpt-4o-mini"
	}
	return &OpenAI{apiKey: apiKey, model: model}
}
ai.NewProvider function · go · L18-L35 (18 LOC)
internal/ai/provider.go
func NewProvider(providerName, apiKey, model, ollamaURL string) (Provider, error) {
	switch strings.ToLower(providerName) {
	case "claude":
		if apiKey == "" {
			return nil, fmt.Errorf("Claude API 키가 필요합니다 (GITDAY_API_KEY 환경변수 또는 설정 파일)")
		}
		return NewClaude(apiKey, model), nil
	case "openai":
		if apiKey == "" {
			return nil, fmt.Errorf("OpenAI API 키가 필요합니다 (GITDAY_API_KEY 환경변수 또는 설정 파일)")
		}
		return NewOpenAI(apiKey, model), nil
	case "ollama":
		return NewOllama(ollamaURL, model), nil
	default:
		return nil, fmt.Errorf("지원하지 않는 AI 프로바이더: %s (claude/openai/ollama)", providerName)
	}
}
ai.BuildPrompt function · go · L38-L54 (17 LOC)
internal/ai/provider.go
func BuildPrompt(results []git.RepoResult, since string) string {
	var sb strings.Builder
	sb.WriteString("다음은 개발자의 Git 커밋 로그입니다. 이 내용을 바탕으로 오늘 한 일을 자연어로 간결하게 요약해주세요.\n")
	sb.WriteString("- 프로젝트별로 핵심 작업을 1-2문장으로 요약\n")
	sb.WriteString("- 마지막에 전체적인 한줄 요약 추가\n")
	sb.WriteString("- 한국어로 작성\n\n")

	for _, r := range results {
		sb.WriteString(fmt.Sprintf("## %s (%d commits)\n", r.Name, len(r.Commits)))
		for _, c := range r.Commits {
			sb.WriteString(fmt.Sprintf("- %s\n", c.Message))
		}
		sb.WriteString("\n")
	}

	return sb.String()
}
git.CollectLogs function · go · L29-L58 (30 LOC)
internal/git/log.go
func CollectLogs(repos []string, since, until time.Time, author string) ([]RepoResult, error) {
	var (
		mu      sync.Mutex
		wg      sync.WaitGroup
		results []RepoResult
	)

	for _, repo := range repos {
		wg.Add(1)
		go func(repoPath string) {
			defer wg.Done()

			commits, err := getCommits(repoPath, since, until, author)
			if err != nil || len(commits) == 0 {
				return
			}

			mu.Lock()
			results = append(results, RepoResult{
				Name:    filepath.Base(repoPath),
				Path:    repoPath,
				Commits: commits,
			})
			mu.Unlock()
		}(repo)
	}

	wg.Wait()
	return results, nil
}
git.getCommits function · go · L62-L85 (24 LOC)
internal/git/log.go
func getCommits(repoPath string, since, until time.Time, author string) ([]Commit, error) {
	format := "%H" + separator + "%s" + separator + "%an" + separator + "%aI"
	args := []string{
		"log",
		"--all",
		"--format=" + format,
		"--since=" + since.Format(time.RFC3339),
		"--until=" + until.Format(time.RFC3339),
		"--shortstat",
	}

	if author != "" {
		args = append(args, "--author="+author)
	}

	cmd := exec.Command("git", args...)
	cmd.Dir = repoPath
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	return parseGitLog(string(out))
}
Repobility · severity-and-effort ranking · https://repobility.com
git.parseGitLog function · go · L87-L125 (39 LOC)
internal/git/log.go
func parseGitLog(raw string) ([]Commit, error) {
	var commits []Commit
	lines := strings.Split(strings.TrimSpace(raw), "\n")

	var current *Commit
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// 커밋 라인: hash§§message§§author§§date
		if strings.Contains(line, separator) {
			parts := strings.Split(line, separator)
			if len(parts) < 4 {
				continue
			}

			date, _ := time.Parse(time.RFC3339, parts[3])
			c := Commit{
				Hash:    truncate(parts[0], 7),
				Message: parts[1],
				Author:  parts[2],
				Date:    date,
			}
			commits = append(commits, c)
			current = &commits[len(commits)-1]
			continue
		}

		// shortstat 라인: " 3 files changed, 45 insertions(+), 12 deletions(-)"
		if current != nil && strings.Contains(line, "file") {
			current.Files = parseFileCount(line)
			current = nil
		}
	}

	return commits, nil
}
git.truncate function · go · L127-L132 (6 LOC)
internal/git/log.go
func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n]
}
git.parseFileCount function · go · L134-L143 (10 LOC)
internal/git/log.go
func parseFileCount(stat string) int {
	parts := strings.Fields(stat)
	if len(parts) > 0 {
		n, err := strconv.Atoi(parts[0])
		if err == nil {
			return n
		}
	}
	return 0
}
git.ScanRepos function · go · L11-L55 (45 LOC)
internal/git/scanner.go
func ScanRepos(scanPaths []string, excludes []string) ([]string, error) {
	var repos []string
	seen := make(map[string]bool)

	for _, sp := range scanPaths {
		expanded := expandHome(sp)

		// 해당 경로 자체가 git 레포인 경우
		if isGitRepo(expanded) {
			abs, _ := filepath.Abs(expanded)
			if !seen[abs] {
				repos = append(repos, abs)
				seen[abs] = true
			}
			continue
		}

		// 하위 디렉토리 탐색
		entries, err := os.ReadDir(expanded)
		if err != nil {
			continue
		}

		for _, entry := range entries {
			if !entry.IsDir() {
				continue
			}
			name := entry.Name()

			if shouldExclude(name, excludes) {
				continue
			}

			fullPath := filepath.Join(expanded, name)
			abs, _ := filepath.Abs(fullPath)

			if isGitRepo(abs) && !seen[abs] {
				repos = append(repos, abs)
				seen[abs] = true
			}
		}
	}

	return repos, nil
}
git.shouldExclude function · go · L62-L72 (11 LOC)
internal/git/scanner.go
func shouldExclude(name string, excludes []string) bool {
	if strings.HasPrefix(name, ".") {
		return true
	}
	for _, ex := range excludes {
		if name == ex {
			return true
		}
	}
	return false
}
git.expandHome function · go · L74-L83 (10 LOC)
internal/git/scanner.go
func expandHome(path string) string {
	if strings.HasPrefix(path, "~/") {
		home, err := os.UserHomeDir()
		if err != nil {
			return path
		}
		return filepath.Join(home, path[2:])
	}
	return path
}
notify.SendSlack function · go · L13-L38 (26 LOC)
internal/notify/slack.go
func SendSlack(ctx context.Context, webhookURL, text string) error {
	body := map[string]string{"text": text}
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewReader(jsonBody))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return fmt.Errorf("Slack 웹훅 전송 실패: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		respBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("Slack 에러 (%d): %s", resp.StatusCode, string(respBody))
	}

	return nil
}
output.ToMarkdown function · go · L12-L50 (39 LOC)
internal/output/markdown.go
func ToMarkdown(results []git.RepoResult, since, until time.Time, summary string) string {
	var sb strings.Builder

	weekday := weekdayKo(since.Weekday())
	sb.WriteString(fmt.Sprintf("# 📅 %s (%s)\n\n", since.Format("2006-01-02"), weekday))

	totalCommits := 0
	totalFiles := 0

	for _, r := range results {
		commitCount := len(r.Commits)
		totalCommits += commitCount

		fileCount := 0
		for _, c := range r.Commits {
			fileCount += c.Files
		}
		totalFiles += fileCount

		sb.WriteString(fmt.Sprintf("## %s (%d commits)\n\n", r.Name, commitCount))
		for _, c := range r.Commits {
			if c.Files > 0 {
				sb.WriteString(fmt.Sprintf("- `%s` %s (%d files)\n", c.Hash, c.Message, c.Files))
			} else {
				sb.WriteString(fmt.Sprintf("- `%s` %s\n", c.Hash, c.Message))
			}
		}
		sb.WriteString("\n")
	}

	sb.WriteString(fmt.Sprintf("---\n\n📊 **총 %d commits | %d개 프로젝트 | %d files changed**\n",
		totalCommits, len(results), totalFiles))

	if summary != "" {
		sb.WriteString(fmt.Sprintf("\n## 📝 요약\n\n%
All rows scored by the Repobility analyzer (https://repobility.com)
output.PrintReport function · go · L41-L95 (55 LOC)
internal/output/terminal.go
func PrintReport(results []git.RepoResult, since, until time.Time, compact bool) {
	// 헤더
	weekday := weekdayKo(since.Weekday())
	header := fmt.Sprintf("📅 %s (%s)", since.Format("2006-01-02"), weekday)
	fmt.Println(titleStyle.Render(header))
	fmt.Println()

	totalCommits := 0
	totalFiles := 0

	for _, r := range results {
		commitCount := len(r.Commits)
		totalCommits += commitCount

		fileCount := 0
		for _, c := range r.Commits {
			fileCount += c.Files
		}
		totalFiles += fileCount

		// 레포 헤더
		repoHeader := fmt.Sprintf("━━ %s (%d commits) ", r.Name, commitCount)
		padding := 50 - len(repoHeader)
		if padding < 3 {
			padding = 3
		}
		repoHeader += strings.Repeat("━", padding)
		fmt.Println(repoStyle.Render(repoHeader))

		// 커밋 목록
		if compact {
			// 간략: 첫 3개만
			limit := 3
			if commitCount < limit {
				limit = commitCount
			}
			for _, c := range r.Commits[:limit] {
				printCommit(c)
			}
			if commitCount > 3 {
				fmt.Printf("  %s\n", statStyle.Render(fmt.Sprintf("... +%d
output.printCommit function · go · L97-L107 (11 LOC)
internal/output/terminal.go
func printCommit(c git.Commit) {
	hash := hashStyle.Render(c.Hash)
	msg := msgStyle.Render(c.Message)

	if c.Files > 0 {
		stat := statStyle.Render(fmt.Sprintf("(%d files)", c.Files))
		fmt.Printf("  %s %s %s\n", hash, msg, stat)
	} else {
		fmt.Printf("  %s %s\n", hash, msg)
	}
}