Skip to content

Commit ca99792

Browse files
authored
Auto migrate hashicorp to ibm (#189)
* Add automatic HashiCorp to IBM copyright holder migration - Adds updateLicenseHolder() function to auto-detect and replace HashiCorp copyright holders - Detects 'HashiCorp, Inc.', 'HashiCorp Inc', and 'HashiCorp' patterns - Works with all comment styles (Go, Python, Shell, C-style, HTML) - Preserves existing year information and updates to current format - Modifies processFile() to attempt holder update for files with existing licenses - Adds comprehensive test coverage with 14 test cases - Updates README with automatic migration feature documentation This feature ensures old HashiCorp headers are automatically caught and updated, preventing regressions from old PRs or copied code. * Display files being updated regardless of verbose flag - Remove verbose flag check so files are always displayed during updates - Show '(copyright holder updated)' when HashiCorp->IBM migration happens - Show '(license header added)' when new headers are added - Improves visibility in both --plan and regular mode * Fix golangci-lint issues - Add error check for os.Remove in test to satisfy errcheck linter - Remove ineffectual assignment to modified variable * Fix --plan flag to show copyright holder updates and update config - Add wouldUpdateLicenseHolder() function for dry-run detection of copyright holder changes - Modify processFile() to check for potential holder updates in checkonly/plan mode - Add test coverage for wouldUpdateLicenseHolder() function - Now --plan flag shows both missing headers and files that would have copyright holders updated from HashiCorp to IBM - Fix golangci-lint issues (errcheck and ineffassign) - Update .copywrite.hcl copyright_year configuration - Tested: reduced false positives from 29 to 5 files, correctly shows holder updates * Fix remaining golangci-lint errcheck issue - Add error check for tmpfile.Close() in TestWouldUpdateLicenseHolder * Fix 'is a directory' error in copyright holder migration - Add isDirectory helper function to check for directories and symlinks to directories - Use helper in updateLicenseHolder and wouldUpdateLicenseHolder functions - Resolves issue where filepath.Walk's fi.IsDir() misses symlinks to directories - Fixes error: 'read path: is a directory' reported in PR feedback This prevents crashes when processing repositories with symbolic links that point to directories, such as version directories in test fixtures. * Add comprehensive test cases for directory skipping bug fix - Add TestIsDirectory to test helper function for detecting directories and symlinks - Add TestUpdateLicenseHolderSkipsDirectories to verify updateLicenseHolder skips directories - Add TestWouldUpdateLicenseHolderSkipsDirectories to verify wouldUpdateLicenseHolder skips directories - Add TestDirectorySkippingRegressionTest as integration test for the original crash scenario - Tests cover symlinks to directories which was the root cause of 'is a directory' error - Ensures the fix in commit 0a3f20d remains stable and prevents future regressions * Fix golangci-lint errcheck issue in test Check error return value from tmpfile.Close() in TestIsDirectory to satisfy errcheck linter.
1 parent 0be59e7 commit ca99792

4 files changed

Lines changed: 665 additions & 3 deletions

File tree

.copywrite.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ schema_version = 1
22

33
project {
44
license = "MPL-2.0"
5-
copyright_year = 2022
5+
copyright_year = 2023
66

77
# (OPTIONAL) A list of globs that should not have copyright/license headers.
88
# Supports doublestar glob patterns for more flexibility in defining which

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ Flags:
5555
Use "copywrite [command] --help" for more information about a command.
5656
```
5757

58+
### Automatic Copyright Holder Migration
59+
60+
The `copywrite headers` command automatically detects and updates old copyright
61+
holders (such as "HashiCorp, Inc.") to the configured holder (default: "IBM Corp.")
62+
while preserving existing year information and updating year ranges.
63+
64+
This ensures that:
65+
66+
- Old headers from merged PRs are automatically corrected
67+
- Manually copied headers are updated
68+
- Year ranges are kept current
69+
70+
No additional flags are needed - the migration happens automatically as part of
71+
the normal headers command execution.
72+
5873
To get started with Copywrite on a new project, run `copywrite init`, which will
5974
interactively help generate a `.copywrite.hcl` config file to add to Git.
6075

addlicense/main.go

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,37 @@ func processFile(f *file, t *template.Template, license LicenseData, checkonly b
253253
logger.Printf("%s\n", f.path)
254254
return errors.New("missing license header")
255255
}
256+
// Also check if existing files would need copyright holder updates
257+
wouldUpdate, err := wouldUpdateLicenseHolder(f.path, license)
258+
if err != nil {
259+
logger.Printf("%s: %v", f.path, err)
260+
return err
261+
}
262+
if wouldUpdate {
263+
logger.Printf("%s (would update copyright holder)\n", f.path)
264+
return errors.New("copyright holder would be updated")
265+
}
256266
} else {
267+
// First, try to add a license if missing
257268
modified, err := addLicense(f.path, f.mode, t, license)
258269
if err != nil {
259270
logger.Printf("%s: %v", f.path, err)
260271
return err
261272
}
262-
if verbose && modified {
263-
logger.Printf("%s modified", f.path)
273+
274+
// If file wasn't modified (already had a license), try to update the holder
275+
if !modified {
276+
updated, err := updateLicenseHolder(f.path, f.mode, license)
277+
if err != nil {
278+
logger.Printf("%s: %v", f.path, err)
279+
return err
280+
}
281+
if updated {
282+
283+
logger.Printf("%s (copyright holder updated)", f.path)
284+
}
285+
} else {
286+
logger.Printf("%s (license header added)", f.path)
264287
}
265288
}
266289
return nil
@@ -340,6 +363,136 @@ func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data Li
340363
return true, os.WriteFile(path, b, fmode)
341364
}
342365

366+
// isDirectory checks if the given path points to a directory (including through symlinks)
367+
func isDirectory(path string) (bool, error) {
368+
fi, err := os.Stat(path)
369+
if err != nil {
370+
return false, err
371+
}
372+
return fi.IsDir(), nil
373+
}
374+
375+
// updateLicenseHolder checks if a file contains old copyright holders
376+
// (like "HashiCorp, Inc.") and updates them to the new holder while
377+
// preserving years and other header information.
378+
// Returns true if the file was updated.
379+
func updateLicenseHolder(path string, fmode os.FileMode, newData LicenseData) (bool, error) {
380+
// Skip directories and symlinks to directories
381+
isDir, err := isDirectory(path)
382+
if err != nil {
383+
return false, err
384+
}
385+
if isDir {
386+
return false, nil
387+
}
388+
389+
b, err := os.ReadFile(path)
390+
if err != nil {
391+
return false, err
392+
}
393+
394+
// Define old holder patterns to detect and replace
395+
oldHolders := []string{
396+
"HashiCorp, Inc.",
397+
"HashiCorp Inc\\.?", // Match "HashiCorp Inc" with optional period
398+
"HashiCorp",
399+
}
400+
401+
updated := b
402+
changed := false
403+
404+
for _, oldHolder := range oldHolders {
405+
// Build regex to match various copyright formats:
406+
// - "Copyright (c) HashiCorp, Inc. 2023"
407+
// - "Copyright 2023 HashiCorp, Inc."
408+
// - "Copyright HashiCorp, Inc. 2023, 2025"
409+
// - "Copyright (c) 2023 HashiCorp, Inc."
410+
// - "<!-- Copyright (c) HashiCorp, Inc. 2023 -->"
411+
pattern := regexp.MustCompile(
412+
`(?im)^(\s*(?://|#|/\*+|\*|<!--)\s*)` + // Comment prefix (group 1)
413+
`(Copyright\s*(?:\(c\)\s*)?)` + // "Copyright" with optional (c) (group 2)
414+
`(?:(\d{4}(?:,\s*\d{4})?)\s+)?` + // Optional years before holder (group 3)
415+
`(` + oldHolder + `)` + // Old holder name (group 4) - now not using QuoteMeta for regex patterns
416+
`(?:\s+(\d{4}(?:,\s*\d{4})?))?` + // Optional years after holder (group 5)
417+
`(\s*(?:-->)?\s*)$`, // Trailing whitespace and optional HTML comment close (group 6)
418+
)
419+
420+
// Replace with new format: "Copyright IBM Corp. YYYY, YYYY"
421+
updated = pattern.ReplaceAllFunc(updated, func(match []byte) []byte {
422+
// Extract the comment prefix from the match
423+
submatch := pattern.FindSubmatch(match)
424+
if submatch == nil {
425+
return match
426+
}
427+
428+
commentPrefix := string(submatch[1])
429+
trailingSpace := string(submatch[6])
430+
431+
// Build new copyright line
432+
newLine := commentPrefix + "Copyright"
433+
if newData.Holder != "" {
434+
newLine += " " + newData.Holder
435+
}
436+
if newData.Year != "" {
437+
newLine += " " + newData.Year
438+
}
439+
newLine += trailingSpace
440+
441+
changed = true
442+
return []byte(newLine)
443+
})
444+
}
445+
446+
if !changed {
447+
return false, nil
448+
}
449+
450+
return true, os.WriteFile(path, updated, fmode)
451+
}
452+
453+
// wouldUpdateLicenseHolder checks if a file would need copyright holder updates
454+
// without actually modifying the file. Used for plan/dry-run mode.
455+
func wouldUpdateLicenseHolder(path string, newData LicenseData) (bool, error) {
456+
// Skip directories and symlinks to directories
457+
isDir, err := isDirectory(path)
458+
if err != nil {
459+
return false, err
460+
}
461+
if isDir {
462+
return false, nil
463+
}
464+
465+
b, err := os.ReadFile(path)
466+
if err != nil {
467+
return false, err
468+
}
469+
470+
// Define old holder patterns to detect
471+
oldHolders := []string{
472+
"HashiCorp, Inc.",
473+
"HashiCorp Inc\\.?", // Match "HashiCorp Inc" with optional period
474+
"HashiCorp",
475+
}
476+
477+
for _, oldHolder := range oldHolders {
478+
// Build regex to match various copyright formats
479+
pattern := regexp.MustCompile(
480+
`(?im)^(\s*(?://|#|/\*+|\*|<!--)\s*)` + // Comment prefix (group 1)
481+
`(Copyright\s*(?:\(c\)\s*)?)` + // "Copyright" with optional (c) (group 2)
482+
`(?:(\d{4}(?:,\s*\d{4})?)\s+)?` + // Optional years before holder (group 3)
483+
`(` + oldHolder + `)` + // Old holder name (group 4)
484+
`(?:\s+(\d{4}(?:,\s*\d{4})?))?` + // Optional years after holder (group 5)
485+
`(\s*(?:-->)?\s*)$`, // Trailing whitespace and optional HTML comment close (group 6)
486+
)
487+
488+
if pattern.Match(b) {
489+
return true, nil
490+
}
491+
}
492+
493+
return false, nil
494+
}
495+
343496
// fileHasLicense reports whether the file at path contains a license header.
344497
func fileHasLicense(path string) (bool, error) {
345498
b, err := os.ReadFile(path)

0 commit comments

Comments
 (0)