Skip to content

Commit 86a09c8

Browse files
committed
Add auto-detection of copyright year from git history
- Add detectFirstCommitYear() function to auto-detect first commit year - Update FormatCopyrightYears() to use auto-detection when copyright_year not set - Graceful fallback to current year if git unavailable - Explicit copyright_year in config always takes precedence - Add comprehensive unit tests for auto-detection - Update README documentation to explain auto-detection behavior This enhancement eliminates the need to manually configure copyright_year for existing repositories, reducing configuration time from ~30min to ~2min per repo for the TF-337 rollout.
1 parent 3846a8b commit 86a09c8

File tree

3 files changed

+105
-14
lines changed

3 files changed

+105
-14
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ project {
101101
# This is used as the starting year in copyright statements
102102
# If set and different from current year, headers will show: "copyright_year, current_year"
103103
# If set and same as current year, headers will show: "current_year"
104-
# If not set (0), it will be auto-detected from GitHub or use current year only
105-
# Default: <the year the repo was first created>
104+
# If not set (0), the tool will auto-detect from git history (first commit year)
105+
# If auto-detection fails, it will fallback to current year only
106+
# Default: 0 (auto-detect)
106107
# copyright_year = 0
107108
108109
# (OPTIONAL) A list of globs that should not have copyright or license headers .

config/config.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ package config
66
import (
77
"fmt"
88
"os"
9+
"os/exec"
910
"path/filepath"
1011
"strconv"
12+
"strings"
1113
"time"
1214

1315
"github.com/knadh/koanf"
@@ -240,22 +242,64 @@ func (c *Config) GetConfigPath() string {
240242
return c.absCfgPath
241243
}
242244

245+
// detectFirstCommitYear attempts to auto-detect the first commit year from git history.
246+
// Returns 0 if detection fails or git is not available.
247+
func (c *Config) detectFirstCommitYear() int {
248+
// Try to get the year of the first commit
249+
cmd := exec.Command("git", "log", "--reverse", "--format=%ad", "--date=format:%Y")
250+
cmd.Dir = filepath.Dir(c.absCfgPath)
251+
252+
// If no config path set, use current directory
253+
if c.absCfgPath == "" {
254+
if wd, err := os.Getwd(); err == nil {
255+
cmd.Dir = wd
256+
}
257+
}
258+
259+
output, err := cmd.Output()
260+
if err != nil {
261+
// Git command failed (not a git repo, git not installed, etc.)
262+
return 0
263+
}
264+
265+
// Parse the first line (first commit year)
266+
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
267+
if len(lines) == 0 || lines[0] == "" {
268+
return 0
269+
}
270+
271+
year, err := strconv.Atoi(strings.TrimSpace(lines[0]))
272+
if err != nil || year < 1970 || year > time.Now().Year() {
273+
// Invalid year
274+
return 0
275+
}
276+
277+
return year
278+
}
279+
243280
// FormatCopyrightYears returns a formatted year string for copyright statements.
244-
// If copyrightYear is 0 or equals current year, returns current year only.
281+
// If copyrightYear is 0, attempts to auto-detect from git history.
282+
// If copyrightYear equals current year, returns current year only.
245283
// Otherwise returns "copyrightYear, currentYear" format (e.g., "2023, 2025").
246284
func (c *Config) FormatCopyrightYears() string {
247285
currentYear := time.Now().Year()
248-
249-
// If no copyright year is set, use current year only
250-
if c.Project.CopyrightYear == 0 {
251-
return strconv.Itoa(currentYear)
286+
copyrightYear := c.Project.CopyrightYear
287+
288+
// If no copyright year is set, try auto-detection from git
289+
if copyrightYear == 0 {
290+
if detectedYear := c.detectFirstCommitYear(); detectedYear > 0 {
291+
copyrightYear = detectedYear
292+
} else {
293+
// Fallback to current year if auto-detection fails
294+
return strconv.Itoa(currentYear)
295+
}
252296
}
253297

254298
// If copyright year equals current year, return single year
255-
if c.Project.CopyrightYear == currentYear {
299+
if copyrightYear == currentYear {
256300
return strconv.Itoa(currentYear)
257301
}
258302

259303
// Return year range: "startYear, currentYear"
260-
return fmt.Sprintf("%d, %d", c.Project.CopyrightYear, currentYear)
304+
return fmt.Sprintf("%d, %d", copyrightYear, currentYear)
261305
}

config/config_test.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package config
55

66
import (
77
"fmt"
8+
"os"
89
"path/filepath"
910
"strconv"
1011
"strings"
@@ -381,11 +382,6 @@ func Test_FormatCopyrightYears(t *testing.T) {
381382
copyrightYear int
382383
expectedOutput string
383384
}{
384-
{
385-
description: "No copyright year set (0) should return current year only",
386-
copyrightYear: 0,
387-
expectedOutput: strconv.Itoa(currentYear),
388-
},
389385
{
390386
description: "Copyright year equals current year should return single year",
391387
copyrightYear: currentYear,
@@ -414,3 +410,53 @@ func Test_FormatCopyrightYears(t *testing.T) {
414410
})
415411
}
416412
}
413+
414+
func Test_FormatCopyrightYears_AutoDetect(t *testing.T) {
415+
currentYear := time.Now().Year()
416+
417+
t.Run("Auto-detect from git when copyright_year not set", func(t *testing.T) {
418+
c := MustNew()
419+
c.Project.CopyrightYear = 0
420+
421+
// Set config path to this repo's directory for git detection
422+
c.absCfgPath = filepath.Join(getCurrentDir(t), ".copywrite.hcl")
423+
424+
actualOutput := c.FormatCopyrightYears()
425+
426+
// Should auto-detect and return a year range (this repo was created before 2025)
427+
// The format should be "YYYY, currentYear" where YYYY < currentYear
428+
assert.Contains(t, actualOutput, ",", "Should contain year range when auto-detected from git")
429+
assert.Contains(t, actualOutput, strconv.Itoa(currentYear), "Should contain current year")
430+
431+
// Parse and validate the detected year
432+
parts := strings.Split(actualOutput, ", ")
433+
if len(parts) == 2 {
434+
detectedYear, err := strconv.Atoi(parts[0])
435+
assert.Nil(t, err, "First part should be a valid year")
436+
assert.True(t, detectedYear >= 2020 && detectedYear <= currentYear,
437+
"Detected year should be reasonable (between 2020 and current year)")
438+
}
439+
})
440+
441+
t.Run("Fallback to current year when git not available", func(t *testing.T) {
442+
c := MustNew()
443+
c.Project.CopyrightYear = 0
444+
445+
// Set config path to non-existent directory (git will fail)
446+
c.absCfgPath = "/nonexistent/path/.copywrite.hcl"
447+
448+
actualOutput := c.FormatCopyrightYears()
449+
450+
// Should fallback to current year only
451+
assert.Equal(t, strconv.Itoa(currentYear), actualOutput,
452+
"Should fallback to current year when git detection fails")
453+
})
454+
}
455+
456+
// Helper function to get current directory
457+
func getCurrentDir(t *testing.T) string {
458+
dir, err := os.Getwd()
459+
assert.Nil(t, err, "Should be able to get current directory")
460+
return dir
461+
}
462+

0 commit comments

Comments
 (0)