Skip to content

Commit 71032cd

Browse files
authored
Add a utility to update the years in license comments of modified files (#1261)
* Add a utility to update the years in license comments of modified files * Move license updating utility to a dedicated sub directory * Use `Regex` instead of `NSRegularExpression`
1 parent 4f207a4 commit 71032cd

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 6.1
2+
/*
3+
This source file is part of the Swift.org open source project
4+
5+
Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
Licensed under Apache License v2.0 with Runtime Library Exception
7+
8+
See https://swift.org/LICENSE.txt for license information
9+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
10+
*/
11+
12+
import PackageDescription
13+
14+
let package = Package(
15+
name: "update-license-for-modified-files",
16+
platforms: [
17+
.macOS(.v13)
18+
],
19+
targets: [
20+
.executableTarget(
21+
name: "update-license-for-modified-files"
22+
),
23+
]
24+
)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
// Determine what changes to consider
14+
15+
enum DiffStrategy {
16+
case stagedFiles
17+
case comparingTo(treeish: String)
18+
}
19+
20+
let arguments = ProcessInfo.processInfo.arguments.dropFirst()
21+
let diffStrategy: DiffStrategy
22+
switch arguments.first {
23+
case "-h", "--help":
24+
print("""
25+
OVERVIEW: Update the year in the license comment of modified files
26+
27+
USAGE: swift run update-license-for-modified-files [--staged | <tree-ish>]
28+
29+
To update the year for staged, but not yet committed files, run:
30+
swift run update-license-for-modified-files --staged
31+
32+
To update the year for all already committed changes that are different from the 'main' branch, run:
33+
swift run update-license-for-modified-files
34+
35+
To update the year for the already committed changes in the last commit, run:
36+
swift run update-license-for-modified-files HEAD~
37+
38+
You can specify any other branch or commit for this argument but I don't know if there's a real use case for doing so.
39+
""")
40+
exit(0)
41+
42+
case nil:
43+
diffStrategy = .comparingTo(treeish: "main")
44+
case "--staged", "--cached":
45+
diffStrategy = .stagedFiles
46+
case let treeish?:
47+
diffStrategy = .comparingTo(treeish: treeish)
48+
}
49+
50+
// Find which files are modified
51+
52+
let repoURL: URL = {
53+
let url = URL(fileURLWithPath: #filePath)
54+
.deletingLastPathComponent() // main.swift
55+
.deletingLastPathComponent() // update-license-for-modified-files
56+
.deletingLastPathComponent() // Sources
57+
.deletingLastPathComponent() // update-license-comments
58+
.deletingLastPathComponent() // bin
59+
guard FileManager.default.fileExists(atPath: url.appendingPathComponent("Package.swift").path) else {
60+
fatalError("The path to the Swift-DocC source root has changed. This should only happen if the 'update-license-comments' sources have moved relative to the Swift-DocC repo.")
61+
}
62+
return url
63+
}()
64+
65+
let modifiedFiles = try findModifiedFiles(in: repoURL, strategy: diffStrategy)
66+
67+
// Update the years in the license comment where necessary
68+
69+
// An optional lower range of years for the license comment (including the hyphen)
70+
// │ The upper range of years for the license comment
71+
// │ │ The markdown files don't have a "." but the Swift files do
72+
// │ │ │ The markdown files capitalize the P but the Swift files don't
73+
// │ │ │ │
74+
// ╭─────┴──────╮╭────┴─────╮ ╭┴╮ ╭┴─╮
75+
let licenseRegex = /Copyright \(c\) (20[0-9]{2}-)?(20[0-9]{2}) Apple Inc\.? and the Swift [Pp]roject authors/
76+
77+
let currentYear = Calendar.current.component(.year, from: .now)
78+
79+
for file in modifiedFiles {
80+
guard var content = try? String(contentsOf: file, encoding: .utf8),
81+
let licenseMatch = try? licenseRegex.firstMatch(in: content)
82+
else {
83+
// Didn't encounter a license comment in this file, do nothing
84+
continue
85+
}
86+
87+
let upperYearSubstring = licenseMatch.2
88+
guard let upperYear = Int(upperYearSubstring) else {
89+
print("Couldn't find license year in \(content[licenseMatch.range])")
90+
continue
91+
}
92+
93+
guard upperYear < currentYear else {
94+
// The license for this file is already up to date. No need to update it.
95+
continue
96+
}
97+
98+
if licenseMatch.1 == nil {
99+
// The existing license comment only contains a single year. Add the new year after
100+
content.insert(contentsOf: "-\(currentYear)", at: upperYearSubstring.endIndex)
101+
} else {
102+
// The existing license comment contains both a start year and an end year. Update the second year.
103+
content.replaceSubrange(upperYearSubstring.startIndex ..< upperYearSubstring.endIndex, with: "\(currentYear)")
104+
}
105+
try content.write(to: file, atomically: true, encoding: .utf8)
106+
}
107+
108+
// MARK: Modified files
109+
110+
private func findModifiedFiles(in repoURL: URL, strategy: DiffStrategy) throws -> [URL] {
111+
let diffCommand = Process()
112+
diffCommand.currentDirectoryURL = repoURL
113+
diffCommand.executableURL = URL(fileURLWithPath: "/usr/bin/git")
114+
115+
let comparisonFlag: String = switch strategy {
116+
case .stagedFiles:
117+
"--cached"
118+
case .comparingTo(let treeish):
119+
treeish
120+
}
121+
122+
diffCommand.arguments = ["diff", "--name-only", comparisonFlag]
123+
124+
let output = Pipe()
125+
diffCommand.standardOutput = output
126+
127+
try diffCommand.run()
128+
129+
guard let outputData = try output.fileHandleForReading.readToEnd(),
130+
let outputString = String(data: outputData, encoding: .utf8)
131+
else {
132+
return []
133+
}
134+
135+
return outputString
136+
.components(separatedBy: .newlines)
137+
.compactMap { line in
138+
guard !line.isEmpty else { return nil }
139+
return repoURL.appendingPathComponent(line, isDirectory: false)
140+
}
141+
}

0 commit comments

Comments
 (0)