Function bodies 25 total
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.ToMarkdowncmd.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)
}
rescmd.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.Prcmd.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.Scmd.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"`
} `jsonai.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("... +%doutput.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)
}
}