@@ -99,6 +99,28 @@ def extract_license_header(lines: list[str], start_idx: int) -> tuple[str, int]:
9999 return "" .join (header_lines ), end_idx - start_idx
100100
101101
102+ def parse_header_start_year (header : str ) -> int | None :
103+ """Extract the start year from an existing license header.
104+
105+ This function parses the copyright year from SPDX headers, handling both
106+ single-year and year-range formats.
107+
108+ Args:
109+ header: The existing license header text
110+
111+ Returns:
112+ The start year as an integer, or None if no valid year found.
113+
114+ Examples:
115+ - "Copyright (c) 2026" -> 2026
116+ - "Copyright (c) 2025-2026" -> 2025
117+ """
118+ match = re .search (r"Copyright \(c\) (\d{4})(?:-\d{4})?" , header )
119+ if match :
120+ return int (match .group (1 ))
121+ return None
122+
123+
102124def _analyze_file_header (lines : list [str ]) -> HeaderAnalysis :
103125 """Analyze a file to find its current header state."""
104126 if not lines :
@@ -260,13 +282,31 @@ def get_file_creation_year(file_path: Path) -> int | None:
260282 return None
261283
262284
263- def get_copyright_year_string (file_path : Path , current_year : int ) -> str :
285+ def get_copyright_year_string (file_path : Path , current_year : int , existing_header : str = "" ) -> str :
264286 """Generate the copyright year string for a file.
265287
288+ Uses the existing license header as the source of truth for the start year.
289+ This ensures consistency across rebases, squash merges, and different branch
290+ states. Git history is only used as a fallback for files without headers.
291+
292+ Args:
293+ file_path: Path to the file being processed
294+ current_year: The current year to use as the end year
295+ existing_header: The existing license header content, if any
296+
266297 Returns:
267298 - Just the current year if file was created this year (e.g., "2026")
268299 - A range if file was created in a previous year (e.g., "2025-2026")
269300 """
301+ # First, try to get start year from existing header (source of truth)
302+ if existing_header :
303+ start_year = parse_header_start_year (existing_header )
304+ if start_year is not None :
305+ if start_year >= current_year :
306+ return str (current_year )
307+ return f"{ start_year } -{ current_year } "
308+
309+ # Fallback to git history for new files without headers
270310 creation_year = get_file_creation_year (file_path )
271311
272312 if creation_year is None or creation_year >= current_year :
@@ -298,7 +338,16 @@ def main(path: Path, check_only: bool = False) -> tuple[int, int, int, list[Path
298338
299339 processed += 1
300340
301- copyright_year = get_copyright_year_string (file_path , current_year )
341+ # Read file and analyze existing header first (source of truth for start year)
342+ try :
343+ content = file_path .read_text (encoding = "utf-8" )
344+ lines = content .splitlines (keepends = True )
345+ analysis = _analyze_file_header (lines )
346+ existing_header = analysis .existing_header
347+ except (UnicodeDecodeError , PermissionError ):
348+ existing_header = ""
349+
350+ copyright_year = get_copyright_year_string (file_path , current_year , existing_header )
302351 license_header = generate_license_header (copyright_year )
303352
304353 if check_only :
0 commit comments