-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathGitIgnore.swift
263 lines (243 loc) · 11.8 KB
/
GitIgnore.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
//
// GitIgnore.swift
// AuroraEditor
//
// Created by Nanashi Li on 2022/08/13.
// Copyright © 2022 Aurora Company. All rights reserved.
// This source code is restricted for Aurora Editor usage only.
//
import Foundation
public struct GitIgnore {
public init() {}
/// Read the contents of the repository's root `.gitignore` file.
///
/// This function reads the contents of the `.gitignore` file located in the root directory of the Git repository. \
/// If the `.gitignore` file exists and has content, it will be returned as a string. \
/// If there is no `.gitignore` file in the repository root, the function will return `nil`.
///
/// - Parameter directoryURL: The URL of the Git repository directory where the `.gitignore` \
/// file is expected to be located.
///
/// - Returns: The contents of the `.gitignore` file as a string if it exists, or `nil` if the file is not present.
///
/// - Throws: An error if there is a problem reading the `.gitignore` file.
///
/// - Example:
/// ```swift
/// let directoryURL = URL(fileURLWithPath: "/path/to/git/repository")
/// if let gitIgnoreContents = try readGitIgnoreAtRoot(directoryURL: directoryURL) {
/// print("Contents of .gitignore:")
/// print(gitIgnoreContents)
/// } else {
/// print(".gitignore file not found in the repository.")
/// }
/// ```
///
/// - Important: Ensure that you have read permissions for the repository directory
/// and that the provided directory is part of a Git repository.
public func readGitIgnoreAtRoot(directoryURL: URL) throws -> String? {
let ignorePath = try String(contentsOf: directoryURL) + ".gitignore"
let content = try String(contentsOf: URL(string: ignorePath)!)
return content
}
/// Persist the given content to the root `.gitignore` file of the repository.
///
/// This function saves the provided text content to the root `.gitignore` file of the Git repository. \
/// If the repository root doesn't contain a `.gitignore` file, one will be created with the specified content. \
/// If a `.gitignore` file already exists, its contents will be overwritten with the new content.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository directory where the root `.gitignore` file \
/// should be located or created.
/// - text: The content to be saved to the `.gitignore` file. This content should follow `.gitignore` \
/// file format rules.
///
/// - Throws: An error if there is a problem creating, writing, or saving the `.gitignore` file.
///
/// - Note: If the `text` is empty, the `.gitignore` file will be deleted if it exists.
///
/// - Example:
/// ```swift
/// let directoryURL = URL(fileURLWithPath: "/path/to/git/repository")
/// let gitIgnoreContents = "*.log\nbuild/"
/// try saveGitIgnore(directoryURL: directoryURL, text: gitIgnoreContents)
/// ```
///
/// - Important: Ensure that you have write permissions for the repository directory and \
/// that the provided directory is part of a Git repository.
func saveGitIgnore(directoryURL: URL,
text: String) throws {
let ignorePath = directoryURL.appendingPathComponent(".gitignore")
if text.isEmpty {
do {
try FileManager.default.removeItem(at: ignorePath)
} catch {
throw error
}
} else {
let fileContents = try formatGitIgnoreContents(text: text,
directoryURL: directoryURL)
try writeFile(at: ignorePath, contents: fileContents)
}
}
/// Add the given pattern or patterns to the root `.gitignore` file of the repository.
///
/// This function appends one or more patterns to the root `.gitignore` file of the specified Git repository. \
/// The provided patterns will be added to the end of the existing `.gitignore` file,
/// and they will be escaped as necessary to ensure proper matching.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository directory where the root `.gitignore` file is located.
/// - patterns: An array of patterns to append to the `.gitignore` file. \
/// Each pattern should be on a separate line.
///
/// - Throws: An error if there is a problem reading, updating, or saving the `.gitignore` file.
///
/// - Note: If the `.gitignore` file does not exist in the repository, \
/// it will be created with the specified patterns.
///
/// - Example:
/// ```swift
/// let directoryURL = URL(fileURLWithPath: "/path/to/git/repository")
/// let patterns = ["*.log", "build/"]
/// try appendIgnoreRule(directoryURL: directoryURL, patterns: patterns)
/// ```
///
/// - Important: Ensure that you have write permissions for the `.gitignore` file \
/// and that the provided directory is part of a Git repository.
public func appendIgnoreRule(directoryURL: URL, patterns: [String]) throws {
let text = try readGitIgnoreAtRoot(directoryURL: directoryURL)
let currentContents = try formatGitIgnoreContents(text: text!,
directoryURL: directoryURL)
let newPatternText = patterns.joined(separator: "\n")
let newText = try formatGitIgnoreContents(text: "\(currentContents)\(newPatternText)",
directoryURL: directoryURL)
try saveGitIgnore(directoryURL: directoryURL, text: newText)
}
/// Convenience method to add the given file path(s) to the repository's `.gitignore` file.
///
/// This function allows you to append one or more file paths to the `.gitignore` file
/// in the specified repository directory. \
/// The provided file paths will be escaped before being added to the `.gitignore` file
/// to ensure they are properly matched.
///
/// - Parameters:
/// - directoryURL: The URL of the directory where the `.gitignore` file is located.
/// - filePaths: An array of file paths to append to the `.gitignore` file.
///
/// - Throws: An error if there is a problem reading or updating the `.gitignore` file.
///
/// - Note: This function is intended for appending file paths to an existing `.gitignore` file. \
/// If the `.gitignore` file does not exist, you should create it first and then use this
/// function to add file paths.
///
/// - Example:
/// ```swift
/// let directoryURL = URL(fileURLWithPath: "/path/to/repository")
/// let filePaths = ["*.log", "build/"]
/// try appendIgnoreFile(directoryURL: directoryURL, filePath: filePaths)
/// ```
///
/// - Important: Ensure that you have write permissions for the `.gitignore`
/// file and that the provided directory is part of a Git repository.
public func appendIgnoreFile(directoryURL: URL,
filePath: [String]) throws {
let escapedFilePaths = filePath.map {
escapeGitSpecialCharacters(pattern: $0)
}
return try appendIgnoreRule(directoryURL: directoryURL, patterns: escapedFilePaths)
}
/// Escapes special characters in a string for use in a `.gitignore` file pattern.
///
/// This function takes a string pattern and escapes any special characters that
/// are used in `.gitignore` file patterns. \
/// The escaped pattern can then be safely used in a `.gitignore` file to match files and directories.
///
/// - Parameter pattern: The string pattern to be escaped.
///
/// - Returns: The escaped string pattern suitable for use in a `.gitignore` file.
///
/// - Note: The special characters that are escaped include `/`, `[`, `]`, `!`, `*`, `#`, and `?`, \
/// as they have special meanings in `.gitignore` patterns.
///
/// - Example:
/// ```swift
/// let pattern = "*.log"
/// let escapedPattern = escapeGitSpecialCharacters(pattern: pattern)
/// // Use the escapedPattern in a .gitignore file.
/// ```
///
/// - Important: The escaped pattern is intended for use in `.gitignore` files. \
/// Ensure that you understand how `.gitignore` patterns work in Git repositories.
public func escapeGitSpecialCharacters(pattern: String) -> String {
// Define the characters that need to be escaped within a regular expression pattern
let specialCharacters = "/[\\[\\]!\\*\\#\\?]/"
// Use regular expression matching and replacement
let escapedPattern = pattern.replacingOccurrences(
of: specialCharacters,
with: "\\\\$0",
options: .regularExpression
)
return escapedPattern
}
/// Format the contents of a `.gitignore` file based on the current Git configuration settings.
///
/// This function takes the text contents of a `.gitignore` file and formats it based on
/// the current Git configuration settings, specifically `core.autocrlf` and `core.safecrlf`. \
/// Depending on these settings, it may normalize line endings to CRLF (`\r\n`) or LF (`\n`),
/// and it ensures that the file ends with the appropriate line ending based on Git's behavior.
///
/// - Parameters:
/// - text: The text contents of the `.gitignore` file to format.
/// - directoryURL: The URL of the directory containing the `.gitignore` file.
///
/// - Throws: An error if there was an issue retrieving Git configuration values or formatting the text.
///
/// - Returns: The formatted text contents of the `.gitignore` file.
///
/// - Note: The `core.autocrlf` setting controls how line endings are handled in the working directory, \
/// while `core.safecrlf` affects Git's behavior when adding files to the index. \
/// This function ensures that the `.gitignore` file adheres to these settings to \
/// prevent conflicts and confusion.
///
/// - Example:
/// ```swift
/// do {
/// let gitIgnoreText = try formatGitIgnoreContents(
//// text: myGitIgnoreText,
//// directoryURL: myProjectDirectoryURL
/// )
/// // Use the formatted gitIgnoreText as needed.
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
///
/// - Important: Ensure that you have a valid Git repository and a `.gitignore` file \
/// in the specified directory before calling this function.
func formatGitIgnoreContents(text: String, directoryURL: URL) throws -> String {
let autocrlf = try Config().getConfigValue(directoryURL: directoryURL, name: "core.autocrlf")
let safecrlf = try Config().getConfigValue(directoryURL: directoryURL, name: "core.safecrlf")
if autocrlf == "true" && safecrlf == "true" {
// Normalize line endings to CRLF (\r\n)
return text.replacingOccurrences(of: #"\r\n|\n\r|\n|\r"#, with: "\r\n", options: .regularExpression)
}
if text.hasSuffix("\n") {
return text
}
if autocrlf == nil {
// Fall back to Git default behavior (append LF)
return text + "\n"
} else {
let linesEndInCRLF = autocrlf == "true"
return text + (linesEndInCRLF ? "\n" : "\r\n")
}
}
internal func writeFile(at path: URL, contents: String) throws {
do {
try contents.write(to: path, atomically: true, encoding: .utf8)
} catch {
throw error
}
}
}