@@ -12,6 +12,7 @@ import (
1212 "regexp"
1313 "strconv"
1414 "strings"
15+ "sync"
1516 "time"
1617)
1718
@@ -72,6 +73,8 @@ func extractAllCopyrightInfo(filePath string) ([]*CopyrightInfo, error) {
7273 return copyrights , scanner .Err ()
7374}
7475
76+ var contentStartsWithCopyright = regexp .MustCompile (`(?i)^copyright\b` )
77+
7578// parseCopyrightLine extracts copyright details from a line
7679// parseCopyrightLine extracts copyright details from a line
7780// inHbsCommentBlock indicates if we're inside a {{! ... }} block (for .hbs files)
@@ -130,7 +133,7 @@ func parseCopyrightLine(line string, lineNum int, filePath string, inHbsCommentB
130133 // Validate content starts with "Copyright"
131134 // Normalize content for the check
132135 content = strings .TrimSpace (content )
133- if ! regexp . MustCompile ( `(?i)^copyright\b` ) .MatchString (content ) {
136+ if ! contentStartsWithCopyright .MatchString (content ) {
134137 return nil
135138 }
136139
@@ -394,8 +397,8 @@ func calculateYearUpdates(
394397 return shouldUpdate , newStartYear , newEndYear
395398}
396399
397- // getRepoRoot finds the git repository root from a given directory
398- func getRepoRoot (workingDir string ) (string , error ) {
400+ // GetRepoRoot finds the git repository root from a given directory
401+ func GetRepoRoot (workingDir string ) (string , error ) {
399402 repoRootOutput , err := executeGitCommand (
400403 workingDir ,
401404 "rev-parse" , "--show-toplevel" ,
@@ -406,15 +409,77 @@ func getRepoRoot(workingDir string) (string, error) {
406409 return strings .TrimSpace (string (repoRootOutput )), nil
407410}
408411
409- // getFileLastCommitYear returns the year of the last commit that modified a file
410- func getFileLastCommitYear (filePath string ) (int , error ) {
411- absPath , err := filepath .Abs (filePath )
412+ // Returns:
413+ // - A map of file paths to their last commit years for all files in the repository
414+ // - The year of the first commit in the repository (or 0 if not found)
415+ // - An error if the git command fails
416+ func buildRepositoryCache (repoRoot string ) (map [string ]int , int , error ) {
417+ cmd := exec .Command ("git" , "log" , "--format=format:%ad" , "--date=format:%Y" , "--name-only" )
418+ cmd .Dir = repoRoot
419+ output , err := cmd .Output ()
412420 if err != nil {
413- return 0 , err
421+ return nil , 0 , err
422+ }
423+
424+ result := make (map [string ]int )
425+ var currentYear int
426+ firstYear := 0
427+
428+ for _ , line := range strings .Split (string (output ), "\n " ) {
429+ line = strings .TrimSpace (line )
430+ if line == "" {
431+ continue
432+ }
433+
434+ if strings .HasPrefix (line , "__CW_YEAR__=" ) {
435+ line = strings .TrimPrefix (line , "__CW_YEAR__=" )
436+ // If it's a 4-digit year
437+ if year , err := strconv .Atoi (line ); err == nil && year > 1900 && year < 2100 {
438+ currentYear = year
439+ if year < firstYear || firstYear == 0 {
440+ firstYear = year
441+ }
442+ }
443+ }
444+ if currentYear > 0 {
445+ // It's a filename - only store first occurrence (most recent)
446+ if _ , exists := result [line ]; ! exists {
447+ result [line ] = currentYear
448+ }
449+ }
414450 }
451+ return result , firstYear , nil
452+ }
415453
416- // Find repository root
417- repoRoot , err := getRepoRoot (filepath .Dir (absPath ))
454+ var (
455+ lastCommitYearsCache map [string ]int
456+ firstCommitYearCached = 0
457+ once sync.Once
458+ )
459+
460+ func InitializeGitCache (repoRoot string ) error {
461+ once .Do (func () {
462+ cache , firstYear , err := buildRepositoryCache (repoRoot )
463+ if err != nil {
464+ lastCommitYearsCache = make (map [string ]int )
465+ } else {
466+ lastCommitYearsCache = cache
467+ firstCommitYearCached = firstYear
468+ }
469+ })
470+ return nil
471+ }
472+
473+ func getCachedFileLastCommitYear (filePath string , repoRoot string ) (int , error ) {
474+ if year , exists := lastCommitYearsCache [filePath ]; exists {
475+ return year , nil
476+ }
477+ return 0 , fmt .Errorf ("file not found in git cache" )
478+ }
479+
480+ // getFileLastCommitYear returns the year of the last commit that modified a file
481+ func getFileLastCommitYear (filePath string , repoRoot string ) (int , error ) {
482+ absPath , err := filepath .Abs (filePath )
418483 if err != nil {
419484 return 0 , err
420485 }
@@ -425,43 +490,36 @@ func getFileLastCommitYear(filePath string) (int, error) {
425490 return 0 , fmt .Errorf ("failed to calculate relative path: %w" , err )
426491 }
427492
428- // Run git log from repo root with relative path
429- output , err := executeGitCommand (
430- repoRoot ,
431- "log" , "-1" , "--format=%ad" , "--date=format:%Y" , "--" , relPath ,
432- )
433- if err != nil {
434- return 0 , err
493+ if cachedYear , err := getCachedFileLastCommitYear (relPath , repoRoot ); err == nil {
494+ return cachedYear , nil
495+ } else {
496+ return 0 , nil
435497 }
436498
437- return parseYearFromGitOutput (output , false )
438499}
439500
440501// GetRepoFirstCommitYear returns the year of the first commit in the repository
441502func GetRepoFirstCommitYear (workingDir string ) (int , error ) {
442503 // Find repository root for consistency
443- repoRoot , err := getRepoRoot (workingDir )
504+ repoRoot , err := GetRepoRoot (workingDir )
444505 if err != nil {
445506 return 0 , err
446507 }
447508
448- output , err := executeGitCommand (repoRoot , "log" , "--reverse" , "--format=%ad" , "--date=format:%Y" )
449- if err != nil {
450- return 0 , err
451- }
509+ _ = InitializeGitCache (repoRoot )
452510
453- return parseYearFromGitOutput ( output , true )
511+ return firstCommitYearCached , nil
454512}
455513
456514// GetRepoLastCommitYear returns the year of the last commit in the repository
457515func GetRepoLastCommitYear (workingDir string ) (int , error ) {
458516 // Find repository root for consistency
459- repoRoot , err := getRepoRoot (workingDir )
517+ repoRoot , err := GetRepoRoot (workingDir )
460518 if err != nil {
461519 return 0 , err
462520 }
463521
464- output , err := executeGitCommand (repoRoot , "log" , "-1" , "--format=%ad" , "--date=format:%Y" )
522+ output , err := executeGitCommand (repoRoot , "log" , "-1" , "--format=__CW_YEAR__= %ad" , "--date=format:%Y" )
465523 if err != nil {
466524 return 0 , err
467525 }
@@ -527,6 +585,16 @@ func evaluateCopyrightUpdates(
527585// If forceCurrentYear is true, forces end year to current year regardless of git history
528586// Returns true if the file was modified
529587func UpdateCopyrightHeader (filePath string , targetHolder string , configYear int , forceCurrentYear bool ) (bool , error ) {
588+ repoRoot , _ := GetRepoRoot (filepath .Dir (filePath ))
589+ repoFirstYear , _ := GetRepoFirstCommitYear (filepath .Dir (filePath ))
590+ return UpdateCopyrightHeaderWithCache (filePath , targetHolder , configYear , forceCurrentYear , repoFirstYear , repoRoot )
591+ }
592+
593+ // UpdateCopyrightHeaderWithCache updates all copyright headers in a file if needed
594+ // If forceCurrentYear is true, forces end year to current year regardless of git history
595+ // repoFirstYear and repoRoot can be provided to avoid repeated git lookups when processing multiple files
596+ // Returns true if the file was modified
597+ func UpdateCopyrightHeaderWithCache (filePath string , targetHolder string , configYear int , forceCurrentYear bool , repoFirstYear int , repoRoot string ) (bool , error ) {
530598 // Skip .copywrite.hcl config file
531599 if filepath .Base (filePath ) == ".copywrite.hcl" {
532600 return false , nil
@@ -555,8 +623,13 @@ func UpdateCopyrightHeader(filePath string, targetHolder string, configYear int,
555623 }
556624
557625 currentYear := time .Now ().Year ()
558- lastCommitYear , _ := getFileLastCommitYear (filePath )
559- repoFirstYear , _ := GetRepoFirstCommitYear (filepath .Dir (filePath ))
626+
627+ // Try to get the last commit year from git if repo is available
628+ lastCommitYear := 0
629+ if repoRoot != "" {
630+ lastCommitYear , _ = getFileLastCommitYear (filePath , repoRoot )
631+ }
632+ // repoFirstYear, _ := GetRepoFirstCommitYear(filepath.Dir(filePath))
560633
561634 // Evaluate which copyrights need updating
562635 updates := evaluateCopyrightUpdates (
@@ -615,6 +688,16 @@ func UpdateCopyrightHeader(filePath string, targetHolder string, configYear int,
615688// If forceCurrentYear is true, forces end year to current year regardless of git history
616689// Returns true if the file has copyrights matching targetHolder that need year updates
617690func NeedsUpdate (filePath string , targetHolder string , configYear int , forceCurrentYear bool ) (bool , error ) {
691+ repoRoot , _ := GetRepoRoot (filepath .Dir (filePath ))
692+ repoFirstYear , _ := GetRepoFirstCommitYear (filepath .Dir (filePath ))
693+ return NeedsUpdateWithCache (filePath , targetHolder , configYear , forceCurrentYear , repoFirstYear , repoRoot )
694+ }
695+
696+ // NeedsUpdateWithCache checks if a file would be updated without actually modifying it
697+ // If forceCurrentYear is true, forces end year to current year regardless of git history
698+ // repoFirstYear and repoRoot can be provided to avoid repeated git lookups when processing multiple files
699+ // Returns true if the file has copyrights matching targetHolder that need year updates
700+ func NeedsUpdateWithCache (filePath string , targetHolder string , configYear int , forceCurrentYear bool , repoFirstCommitYear int , repoRoot string ) (bool , error ) {
618701 // Skip .copywrite.hcl config file
619702 if filepath .Base (filePath ) == ".copywrite.hcl" {
620703 return false , nil
@@ -642,12 +725,14 @@ func NeedsUpdate(filePath string, targetHolder string, configYear int, forceCurr
642725 }
643726
644727 currentYear := time .Now ().Year ()
645- lastCommitYear , _ := getFileLastCommitYear (filePath )
646- repoFirstYear , _ := GetRepoFirstCommitYear (filepath .Dir (filePath ))
728+ lastCommitYear := 0
729+ if ! forceCurrentYear {
730+ lastCommitYear , _ = getFileLastCommitYear (filePath , repoRoot )
731+ }
647732
648733 // Evaluate which copyrights need updating
649734 updates := evaluateCopyrightUpdates (
650- copyrights , targetHolder , configYear , lastCommitYear , currentYear , forceCurrentYear , repoFirstYear ,
735+ copyrights , targetHolder , configYear , lastCommitYear , currentYear , forceCurrentYear , repoFirstCommitYear ,
651736 )
652737
653738 return len (updates ) > 0 , nil
0 commit comments