Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3fe6da2

Browse files
committedJul 13, 2024·
Improved and added new git functions
1 parent 631dc63 commit 3fe6da2

File tree

7 files changed

+445
-244
lines changed

7 files changed

+445
-244
lines changed
 

‎Sources/Version-Control/Base/Actions/GitHub/GitHubActions.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ public enum GitHubViewType: String {
1515

1616
public struct GitHubActions {
1717

18-
internal func getBranchName(directoryURL: URL) throws -> String {
19-
return try Branch().getCurrentBranch(directoryURL: directoryURL)
18+
internal func getBranchName(
19+
directoryURL: URL,
20+
completion: @escaping (Result<String, Error>) -> Void
21+
) {
22+
Task {
23+
do {
24+
let branchName = try await Branch().getCurrentBranch(directoryURL: directoryURL)
25+
completion(.success(branchName))
26+
} catch {
27+
completion(.failure(error))
28+
}
29+
}
2030
}
2131

2232
internal func getCurrentRepositoryGitHubURL(directoryURL: URL) throws -> String {
@@ -52,7 +62,17 @@ public struct GitHubActions {
5262
public func openBranchOnGitHub(viewType: GitHubViewType,
5363
directoryURL: URL) throws {
5464
let htmlURL = try getCurrentRepositoryGitHubURL(directoryURL: directoryURL)
55-
let branchName = try getBranchName(directoryURL: directoryURL)
65+
66+
var branchName = ""
67+
68+
getBranchName(directoryURL: directoryURL) { result in
69+
switch result {
70+
case .success(let name):
71+
branchName = name
72+
case .failure(let error):
73+
branchName = ""
74+
}
75+
}
5676

5777
let urlEncodedBranchName = branchName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
5878

‎Sources/Version-Control/Base/Commands/Branch.swift

Lines changed: 15 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,19 @@ public struct Branch { // swiftlint:disable:this type_body_length
1818
/// - Parameter directoryURL: The URL of the directory where the Git repository is located.
1919
/// - Returns: A string representing the name of the current branch.
2020
/// - Throws: An error if the shell command fails.
21-
public func getCurrentBranch(directoryURL: URL) throws -> String {
22-
return try ShellClient.live().run(
23-
"cd \(directoryURL.relativePath.escapedWhiteSpaces());git branch --show-current"
24-
).removingNewLines()
21+
public func getCurrentBranch(directoryURL: URL) async throws -> String {
22+
let args = [
23+
"branch",
24+
"--show-current"
25+
]
26+
27+
let result = try await GitShell().git(
28+
args: args,
29+
path: directoryURL,
30+
name: #function
31+
)
32+
33+
return result.stdout.removingNewLines()
2534
}
2635

2736
/// Fetches all branches in the given directory, optionally filtering by prefixes.
@@ -31,7 +40,7 @@ public struct Branch { // swiftlint:disable:this type_body_length
3140
/// - prefixes: An array of strings representing branch name prefixes to filter by. Defaults to an empty array.
3241
/// - Returns: An array of `GitBranch` instances representing the fetched branches.
3342
/// - Throws: An error if the shell command fails.
34-
public func getBranches(directoryURL: URL, prefixes: [String] = []) throws -> [GitBranch] {
43+
public func getBranches(directoryURL: URL, prefixes: [String] = []) async throws -> [GitBranch] {
3544
let fields = ["fullName": "%(refname)",
3645
"shortName": "%(refname:short)",
3746
"upstreamShortName": "%(upstream:short)",
@@ -51,7 +60,7 @@ public struct Branch { // swiftlint:disable:this type_body_length
5160
let gitCommand = ["for-each-ref"] + args + prefixArgs
5261

5362
// Execute the git command using the GitShell utility
54-
let result = try GitShell().git(
63+
let result = try await GitShell().git(
5564
args: gitCommand,
5665
path: directoryURL,
5766
name: #function,
@@ -177,77 +186,6 @@ public struct Branch { // swiftlint:disable:this type_body_length
177186
return eligibleBranches
178187
}
179188

180-
/// Retrieves a list of the most recently modified branches, up to a specified limit.
181-
///
182-
/// - Parameters:
183-
/// - directoryURL: The URL of the directory where the Git repository is located.
184-
/// - limit: An integer specifying the maximum number of branches to retrieve.
185-
/// - Returns: An array of strings representing the names of the recent branches.
186-
/// - Throws: An error if the shell command fails.
187-
public func getRecentBranches(directoryURL: URL, limit: Int) throws -> [String] {
188-
let regex = try NSRegularExpression(
189-
// swiftlint:disable:next line_length
190-
pattern: #"^.*? (renamed|checkout)(?:: moving from|\s*) (?:refs/heads/|\s*)(.*?) to (?:refs/heads/|\s*)(.*?)$"#,
191-
options: []
192-
)
193-
194-
let args = [
195-
"log",
196-
"-g",
197-
"--no-abbrev-commit",
198-
"--pretty=oneline",
199-
"HEAD",
200-
"-n",
201-
"2500",
202-
"--"
203-
]
204-
205-
let result = try GitShell().git(args: args,
206-
path: directoryURL,
207-
name: #function)
208-
209-
if result.exitCode == 128 {
210-
// error code 128 is returned if the branch is unborn
211-
return []
212-
}
213-
214-
let lines = result.stdout.components(separatedBy: "\n")
215-
var names = Set<String>()
216-
var excludedNames = Set<String>()
217-
218-
for line in lines {
219-
if let match = regex.firstMatch(
220-
in: line,
221-
options: [],
222-
range: NSRange(location: 0, length: line.utf16.count)
223-
),
224-
match.numberOfRanges == 4 {
225-
let operationTypeRange = Range(match.range(at: 1), in: line)!
226-
let excludeBranchNameRange = Range(match.range(at: 2), in: line)!
227-
let branchNameRange = Range(match.range(at: 3), in: line)!
228-
229-
let operationType = String(line[operationTypeRange])
230-
let excludeBranchName = String(line[excludeBranchNameRange])
231-
let branchName = String(line[branchNameRange])
232-
233-
if operationType == "renamed" {
234-
// exclude intermediate-state renaming branch from recent branches
235-
excludedNames.insert(excludeBranchName)
236-
}
237-
238-
if !excludedNames.contains(branchName) {
239-
names.insert(branchName)
240-
}
241-
}
242-
243-
if names.count >= limit {
244-
break
245-
}
246-
}
247-
248-
return Array(names)
249-
}
250-
251189
func getCommitsOnBranch() {
252190
guard let noCommitsOnBranchRe = try? NSRegularExpression(
253191
pattern: "fatal: your current branch '.*' does not have any commits yet"
@@ -257,59 +195,6 @@ public struct Branch { // swiftlint:disable:this type_body_length
257195
}
258196
}
259197

260-
/// Asynchronously fetches the names and dates of branches checked out after a specified date.
261-
///
262-
/// - Parameters:
263-
/// - directoryURL: The URL of the directory where the Git repository is located.
264-
/// - afterDate: A `Date` object representing the starting point for the search.
265-
/// - Returns: A dictionary mapping branch names to the dates they were checked out.
266-
/// - Throws: An error if the shell command fails.
267-
func getBranchCheckouts(directoryURL: URL, afterDate: Date) async throws -> [String: Date] {
268-
let regexPattern = #"^[a-z0-9]{40}\sHEAD@{(.*)}\scheckout: moving from\s.*\sto\s(.*)$"# // regexr.com/46n1v
269-
let regex = try NSRegularExpression(pattern: regexPattern, options: [])
270-
271-
let args = [
272-
"reflog",
273-
"--date=iso",
274-
"--after=\(afterDate.timeIntervalSince1970)",
275-
"--pretty=%H %gd %gs",
276-
"--grep-reflog=checkout: moving from .* to .*$",
277-
"--"
278-
]
279-
280-
let result = try GitShell().git(args: args,
281-
path: directoryURL,
282-
name: #function)
283-
284-
var checkouts = [String: Date]()
285-
286-
if result.exitCode == 128 {
287-
return checkouts
288-
}
289-
290-
let lines = result.stdout.components(separatedBy: "\n")
291-
for line in lines {
292-
if let match = regex.firstMatch(
293-
in: line,
294-
options: [],
295-
range: NSRange(location: 0, length: line.utf16.count)
296-
),
297-
match.numberOfRanges == 3 {
298-
let timestampRange = Range(match.range(at: 1), in: line)!
299-
let branchNameRange = Range(match.range(at: 2), in: line)!
300-
301-
let timestampString = String(line[timestampRange])
302-
let branchName = String(line[branchNameRange])
303-
304-
if let timestamp = ISO8601DateFormatter().date(from: timestampString) {
305-
checkouts[branchName] = timestamp
306-
}
307-
}
308-
}
309-
310-
return checkouts
311-
}
312-
313198
/// Creates a new branch in the specified directory.
314199
///
315200
/// This function creates a new branch in the specified Git repository directory. It allows
@@ -446,7 +331,6 @@ public struct Branch { // swiftlint:disable:this type_body_length
446331
remoteName: String,
447332
remoteBranchName: String) throws -> Bool {
448333
let args = [
449-
gitNetworkArguments.joined(),
450334
"push",
451335
remoteName,
452336
":\(remoteBranchName)"

‎Sources/Version-Control/Base/Commands/Checkout.swift

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,39 +32,19 @@ public struct GitCheckout {
3232
branch: GitBranch,
3333
enableRecurseSubmodulesFlag: Bool = false
3434
) -> [String] {
35-
var baseArgs: [String] = []
35+
var args = [branch.name]
36+
37+
if branch.type == .remote {
38+
args.append(contentsOf: ["-b", branch.nameWithoutRemote])
39+
}
3640

3741
if enableRecurseSubmodulesFlag {
38-
if branch.type == BranchType.remote {
39-
return baseArgs + [
40-
branch.name,
41-
"-b",
42-
branch.nameWithoutRemote,
43-
"--recurse-submodules",
44-
"--"
45-
]
46-
} else {
47-
return baseArgs + [
48-
branch.name,
49-
"--recurse-submodules",
50-
"--"
51-
]
52-
}
53-
} else {
54-
if branch.type == BranchType.remote {
55-
return baseArgs + [
56-
branch.name,
57-
"-b",
58-
branch.nameWithoutRemote,
59-
"--"
60-
]
61-
} else {
62-
return baseArgs + [
63-
branch.name,
64-
"--"
65-
]
66-
}
42+
args.append("--recurse-submodules")
6743
}
44+
45+
args.append("--")
46+
47+
return args
6848
}
6949

7050
public func getCheckoutOpts( // swiftlint:disable:this function_parameter_count
@@ -153,8 +133,7 @@ public struct GitCheckout {
153133

154134
try GitShell().git(args: args,
155135
path: directoryURL,
156-
name: #function,
157-
options: opts)
136+
name: #function)
158137
return true
159138
}
160139

‎Sources/Version-Control/Base/Commands/GitLog.swift

Lines changed: 167 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,27 @@
6767
/// - Note:
6868
/// This function checks if the file modes indicate a submodule and then determines the submodule status \
6969
/// based on the provided raw Git status string.
70-
func mapSubmoduleStatusFileModes(status: String, srcMode: String, dstMode: String) -> SubmoduleStatus? {
70+
func mapSubmoduleStatusFileModes(
71+
status: String,
72+
srcMode: String,
73+
dstMode: String
74+
) -> SubmoduleStatus? {
7175
let subModuleFileMode = subModuleFileMode
7276

7377
if srcMode == subModuleFileMode && dstMode == subModuleFileMode && status == "M" {
74-
return SubmoduleStatus(commitChanged: true, modifiedChanges: false, untrackedChanges: false)
75-
} else if (srcMode == subModuleFileMode && status == "D") || (dstMode == subModuleFileMode && status == "A") {
76-
return SubmoduleStatus(commitChanged: false, modifiedChanges: false, untrackedChanges: false)
78+
return SubmoduleStatus(
79+
commitChanged: true,
80+
modifiedChanges: false,
81+
untrackedChanges: false
82+
)
83+
} else if (
84+
srcMode == subModuleFileMode && status == "D"
85+
) || (dstMode == subModuleFileMode && status == "A") {
86+
return SubmoduleStatus(
87+
commitChanged: false,
88+
modifiedChanges: false,
89+
untrackedChanges: false
90+
)
7791
}
7892

7993
return nil
@@ -111,7 +125,11 @@
111125
srcMode: String,
112126
dstMode: String) -> AppFileStatus {
113127
let status = rawStatus.trimmingCharacters(in: .whitespaces)
114-
let submoduleStatus = mapSubmoduleStatusFileModes(status: status, srcMode: srcMode, dstMode: dstMode)
128+
let submoduleStatus = mapSubmoduleStatusFileModes(
129+
status: status,
130+
srcMode: srcMode,
131+
dstMode: dstMode
132+
)
115133

116134
switch status {
117135
case "M":
@@ -345,7 +363,11 @@
345363
let result = try GitShell().git(args: args,
346364
path: directoryURL,
347365
name: #function)
348-
let changedData = try parseRawLogWithNumstat(stdout: result.stdout, sha: sha, parentCommitish: "\(sha)^")
366+
let changedData = try parseRawLogWithNumstat(
367+
stdout: result.stdout,
368+
sha: sha,
369+
parentCommitish: "\(sha)^"
370+
)
349371

350372
// Create an instance of IChangesetData from ChangesetData
351373
let changesetData: IChangesetData = IChangesetData(
@@ -357,6 +379,72 @@
357379
return changesetData
358380
}
359381

382+
/// Retrieve the list of files changed in a specific commit.
383+
///
384+
/// This function executes the `git diff-tree` command to fetch the list of files that were changed
385+
/// in a specified commit.
386+
///
387+
/// - Parameters:
388+
/// - directoryURL: The URL of the local Git repository.
389+
/// - sha: The commit hash for which to fetch the list of changed files.
390+
///
391+
/// - Throws: An error if there is a problem accessing the Git repository or executing the Git command.
392+
///
393+
/// - Returns: An array of strings representing the file paths that were changed in the specified commit.
394+
///
395+
/// - Example:
396+
/// ```swift
397+
/// let repoURL = URL(fileURLWithPath: "/path/to/local/repository")
398+
/// let commitHash = "1bccee6a2bffabc29971f3af56b05d749afcc966"
399+
///
400+
/// do {
401+
/// let changedFiles = try getChangedFiles(repoURL: repoURL, sha: commitHash)
402+
/// print("Changed files in commit \(commitHash):")
403+
/// changedFiles.forEach { print($0) }
404+
/// } catch {
405+
/// print("Error retrieving changed files: \(error)")
406+
/// }
407+
/// ```
408+
///
409+
/// - Note:
410+
/// - The function executes the `git diff-tree` command with the specified commit hash
411+
/// to fetch the list of files that were changed in that commit.
412+
/// - Ensure that the provided `repoURL` and `sha` are valid and accessible.
413+
///
414+
/// - Returns: An array of strings representing the file paths that were changed in the specified commit.
415+
public func getChangedFilesBetweenRefs(
416+
directoryURL: URL,
417+
currentSha: String,
418+
sha: String
419+
) async throws -> [String] {
420+
let result = try await GitShell().git(
421+
args: [
422+
"diff",
423+
"--name-only",
424+
currentSha,
425+
sha
426+
],
427+
path: directoryURL,
428+
name: #function
429+
)
430+
431+
guard result.exitCode == 0 else {
432+
throw NSError(
433+
domain: "GitLog",
434+
code: Int(
435+
result.exitCode
436+
),
437+
userInfo: [NSLocalizedDescriptionKey: "Failed to execute git command"]
438+
)
439+
}
440+
441+
let changedFiles = result.stdout.split(separator: "\n").map {
442+
String($0)
443+
}
444+
445+
return changedFiles
446+
}
447+
360448
/**
361449
Parses the raw Git log output with numstat and extracts file changes and line modification statistics.
362450

@@ -410,7 +498,10 @@
410498
parentCommitish: parentCommitish))
411499
number += oldPath != nil ? 3 : 2
412500
} else {
413-
guard let match = line.range(of: "^(\\d+|-)\\t(\\d+|-)", options: .regularExpression) else {
501+
guard let match = line.range(
502+
of: "^(\\d+|-)\\t(\\d+|-)",
503+
options: .regularExpression
504+
) else {
414505
fatalError("Invalid numstat line")
415506
}
416507

@@ -479,8 +570,8 @@
479570

480571
/// Check if merge commits exist after a specified commit reference in a Git repository.
481572
///
482573
/// This function checks if there are any merge commits after a specified commit reference in the given Git repository directory. \
483574
/// If a commit reference is provided, it checks for merge commits in the revision range from that commit to `HEAD`. \
484575
/// If no commit reference is provided, it checks for merge commits in the entire repository history.
485576
///
486577
/// - Parameters:
@@ -527,4 +618,73 @@
527618

528619
return !mergeCommits.isEmpty
529620
}
621+
622+
/// Retrieve the commit hashes between a given commit and the latest commit on a specified upstream branch.
623+
///
624+
/// This function executes the `git log` command to fetch the commit hashes between a given commit SHA
625+
/// and the latest commit on the specified upstream branch.
626+
///
627+
/// - Parameters:
628+
/// - directoryURL: The URL of the directory containing the Git repository.
629+
/// - sha: The commit SHA from which to start listing commits.
630+
/// - upstreamBranch: The upstream branch to compare against (e.g., "upstream/development").
631+
///
632+
/// - Throws: An error if there is a problem accessing the Git repository or executing the Git command.
633+
///
634+
/// - Returns: An array of commit hashes between the given SHA and the latest commit on the specified upstream branch.
635+
///
636+
/// - Example:
637+
/// ```swift
638+
/// let repoURL = "https://github.com/AuroraEditor/AuroraEditor.git"
639+
/// let sha = "1bccee6a2bffabc29971f3af56b05d749afcc966"
640+
/// let upstreamBranch = "upstream/development"
641+
///
642+
/// do {
643+
/// let commits = try getCommitsBetweenSHAAndUpstreamBranch(repoURL: repoURL, sha: sha, upstreamBranch: upstreamBranch)
644+
/// print("Commits between \(sha) and \(upstreamBranch):")
645+
/// for commit in commits {
646+
/// print(commit)
647+
/// }
648+
/// } catch {
649+
/// print("Error retrieving commits: \(error)")
650+
/// }
651+
/// ```
652+
///
653+
/// - Note:
654+
/// - The function executes the `git log` command with the specified repository URL, SHA, and upstream branch
655+
/// to fetch the commit hashes.
656+
/// - Ensure that the provided `repoURL`, `sha`, and `upstreamBranch` are valid and accessible.
657+
///
658+
/// - Returns: An array of commit hashes between the given SHA and the latest commit on the specified upstream branch.
659+
public func getCommitsBetweenSHAAndUpstreamBranch(
660+
directoryURL: URL,
661+
sha: String,
662+
upstreamBranch: String
663+
) throws -> [String] {
664+
let result = try GitShell().git(
665+
args: [
666+
"log",
667+
"--oneline",
668+
"\(sha)..\(upstreamBranch)",
669+
"--pretty=format:%H"
670+
],
671+
path: directoryURL,
672+
name: #function
673+
)
674+
675+
guard result.exitCode == 0 else {
676+
throw NSError(
677+
domain: "Remote",
678+
code: Int(result.exitCode),
679+
userInfo: [NSLocalizedDescriptionKey: "Failed to execute git command"]
680+
)
681+
}
682+
683+
let commits = result.stdout.split(
684+
separator: "\n"
685+
).map {
686+
String($0)
687+
}
688+
return commits
689+
}
530690
}

‎Sources/Version-Control/Base/Commands/Reflog.swift

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@ public struct Reflog {
4242
/// ```
4343
///
4444
/// - Returns: An array of branch names that were recently checked out or renamed, with a maximum count of `limit`.
45-
func getRecentBranches(directoryURL: URL,
46-
limit: Int) throws -> [String] {
47-
// Define a regular expression to match branch names in git log entries
48-
let regexPattern = #"^\w+ \w+(?:: moving from|\s*) (?:refs/heads/|\s*)(.*?) to (?:refs/heads/|\s*)(.*?)$"#
49-
50-
// Create a regular expression
51-
guard let regex = try? NSRegularExpression(pattern: regexPattern, options: []) else {
52-
// FIXME: a call to a never-returning function
53-
throw fatalError("Invalid regex")
54-
}
45+
public func getRecentBranches(
46+
directoryURL: URL,
47+
limit: Int
48+
) async throws -> [String] {
49+
let regex = try NSRegularExpression(
50+
// swiftlint:disable:next line_length
51+
pattern: #"^.*? (renamed|checkout)(?:: moving from|\s*) (?:refs/heads/|\s*)(.*?) to (?:refs/heads/|\s*)(.*?)$"#,
52+
options: []
53+
)
5554

5655
let args = [
5756
"log",
@@ -64,47 +63,66 @@ public struct Reflog {
6463
"--"
6564
]
6665

67-
let result = try GitShell().git(args: args,
68-
path: directoryURL,
69-
name: #function,
70-
options: IGitExecutionOptions(successExitCodes: Set([0, 128])))
66+
let result = try await GitShell().git(
67+
args: args,
68+
path: directoryURL,
69+
name: #function
70+
)
7171

72-
// Check if the git log returned an error code 128 (branch is unborn)
7372
if result.exitCode == 128 {
73+
// error code 128 is returned if the branch is unborn
7474
return []
7575
}
7676

77-
// Split the stdout of git log into lines
78-
let lines = result.stdout.split(separator: "\n")
79-
80-
// Create sets to store branch names and excluded names
81-
var branchNames = Set<String>()
77+
let lines = result.stdout.components(
78+
separatedBy: "\n"
79+
)
80+
var names = Set<String>()
8281
var excludedNames = Set<String>()
8382

8483
for line in lines {
85-
// Try to match the line with the regular expression
8684
if let match = regex.firstMatch(
87-
in: String(line),
85+
in: line,
8886
options: [],
89-
range: NSRange(line.startIndex..., in: line)
90-
) {
91-
let excludeBranchNameRange = Range(match.range(at: 1), in: line)!
92-
let branchNameRange = Range(match.range(at: 2), in: line)!
93-
87+
range: NSRange(
88+
location: 0,
89+
length: line.utf16.count
90+
)
91+
),
92+
match.numberOfRanges == 4 {
93+
let operationTypeRange = Range(
94+
match.range(at: 1),
95+
in: line
96+
)!
97+
let excludeBranchNameRange = Range(
98+
match.range(at: 2),
99+
in: line
100+
)!
101+
let branchNameRange = Range(
102+
match.range(at: 3),
103+
in: line
104+
)!
105+
106+
let operationType = String(line[operationTypeRange])
94107
let excludeBranchName = String(line[excludeBranchNameRange])
95108
let branchName = String(line[branchNameRange])
96109

97-
if !excludedNames.contains(excludeBranchName) {
98-
branchNames.insert(branchName)
110+
if operationType == "renamed" {
111+
// exclude intermediate-state renaming branch from recent branches
112+
excludedNames.insert(excludeBranchName)
99113
}
100114

101-
if branchNames.count == limit {
102-
break
115+
if !excludedNames.contains(branchName) {
116+
names.insert(branchName)
103117
}
104118
}
119+
120+
if names.count >= limit {
121+
break
122+
}
105123
}
106124

107-
return Array(branchNames)
125+
return Array(names)
108126
}
109127

110128
private let noCommitsOnBranchRe = "fatal: your current branch '.*' does not have any commits yet"
@@ -138,22 +156,20 @@ public struct Reflog {
138156
/// ```
139157
///
140158
/// - Returns: A dictionary where keys are branch names, and values are the timestamps of their checkouts.
141-
func getBranchCheckouts(directoryURL: URL,
142-
afterDate: Date) throws -> [String: Date] {
143-
// Regular expression to match reflog entries
159+
func getBranchCheckouts(
160+
directoryURL: URL,
161+
afterDate: Date
162+
) async throws -> [String: Date] {
163+
let regexPattern = #"^[a-z0-9]{40}\sHEAD@{(.*)}\scheckout: moving from\s.*\sto\s(.*)$"#
144164
let regex = try NSRegularExpression(
145-
pattern: #"^[a-z0-9]{40}\sHEAD@{(.*)}\scheckout: moving from\s.*\sto\s(.*)$"#
165+
pattern: regexPattern,
166+
options: []
146167
)
147168

148-
// Format the afterDate as ISO string
149-
let dateFormatter = ISO8601DateFormatter()
150-
let afterDateString = dateFormatter.string(from: afterDate)
151-
152-
// Run the Git reflog command
153169
let args = [
154170
"reflog",
155171
"--date=iso",
156-
"--after=\(afterDateString)",
172+
"--after=\(afterDate.timeIntervalSince1970)",
157173
"--pretty=%H %gd %gs",
158174
"--grep-reflog=checkout: moving from .* to .*$",
159175
"--"
@@ -165,36 +181,33 @@ public struct Reflog {
165181

166182
var checkouts = [String: Date]()
167183

168-
// Check for the edge case
169184
if result.exitCode == 128 {
170185
return checkouts
171186
}
172187

173-
// Split the result stdout into lines
174-
let lines = result.stdout.split(separator: "\n")
175-
188+
let lines = result.stdout.components(separatedBy: "\n")
176189
for line in lines {
177-
// Attempt to match the line with the regex
178190
if let match = regex.firstMatch(
179-
in: String(line),
191+
in: line,
180192
options: [],
181-
range: NSRange(line.startIndex..., in: line)
182-
) {
193+
range: NSRange(
194+
location: 0,
195+
length: line.utf16.count
196+
)
197+
),
198+
match.numberOfRanges == 3 {
183199
let timestampRange = Range(match.range(at: 1), in: line)!
184200
let branchNameRange = Range(match.range(at: 2), in: line)!
185201

186-
// Extract timestamp and branch name from the matched groups
187202
let timestampString = String(line[timestampRange])
188203
let branchName = String(line[branchNameRange])
189204

190-
// Convert the timestamp string to a Date
191-
if let timestamp = dateFormatter.date(from: timestampString) {
205+
if let timestamp = ISO8601DateFormatter().date(from: timestampString) {
192206
checkouts[branchName] = timestamp
193207
}
194208
}
195209
}
196210

197211
return checkouts
198212
}
199-
200213
}

‎Sources/Version-Control/Base/Commands/Remote.swift

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,14 @@
252252
/// - Warning: Be cautious when passing user-provided remote names to this function, \
253253
/// as it may execute arbitrary Git commands. \
254254
/// Ensure that input is properly validated and sanitized to prevent command injection vulnerabilities.
255-
public func getRemoteURL(directoryURL: URL, name: String) throws -> String? {
255+
public func getRemoteURL(directoryURL: URL, name: String) async throws -> String? {
256256

257-
let result = try GitShell().git(args: ["remote",
258-
"get-url",
259-
name],
260-
path: directoryURL,
261-
name: #function,
262-
options: IGitExecutionOptions(successExitCodes: Set([0, 2, 128])))
257+
let result = try await GitShell().git(args: ["remote",
258+
"get-url",
259+
name],
260+
path: directoryURL,
261+
name: #function,
262+
options: IGitExecutionOptions(successExitCodes: Set([0, 2, 128])))
263263

264264
if result.exitCode != 0 {
265265
return nil
@@ -355,4 +355,84 @@
355355

356356
return nil
357357
}
358+
359+
/// Retrieve the latest commit hash and reference for a given remote Git repository and branch.
360+
///
361+
/// This function executes the `git ls-remote` command to fetch the latest commit hash and reference
362+
/// for a specified remote Git repository and branch.
363+
///
364+
/// - Parameters:
365+
/// - repoURL: The URL of the remote Git repository.
366+
/// - branch: The branch for which to fetch the latest commit hash and reference.
367+
///
368+
/// - Throws: An error if there is a problem accessing the Git repository or executing the Git command.
369+
///
370+
/// - Returns: A tuple containing the latest commit hash and reference for the specified branch.
371+
///
372+
/// - Example:
373+
/// ```swift
374+
/// let repoURL = "https://github.com/AuroraEditor/AuroraEditor.git"
375+
/// let branch = "development"
376+
///
377+
/// do {
378+
/// let (commitHash, ref) = try getLatestCommitHashAndRef(repoURL: repoURL, branch: branch)
379+
/// print("Latest Commit Hash: \(commitHash), Reference: \(ref)")
380+
/// } catch {
381+
/// print("Error retrieving latest commit hash and reference: \(error)")
382+
/// }
383+
/// ```
384+
///
385+
/// - Note:
386+
/// - The function executes the `git ls-remote` command with the specified repository URL and branch
387+
/// to fetch the latest commit hash and reference.
388+
/// - Ensure that the provided `repoURL` and `branch` are valid and accessible.
389+
///
390+
/// - Returns: A tuple containing the latest commit hash and reference for the specified branch.
391+
public func getLatestCommitHashAndRef(
392+
directoryURL: URL,
393+
repoURL: String,
394+
branch: String
395+
) async throws -> (commitHash: String, ref: String) {
396+
let result = try await GitShell().git(
397+
args: [
398+
"ls-remote",
399+
repoURL,
400+
branch
401+
],
402+
path: directoryURL,
403+
name: #function
404+
)
405+
406+
guard result.exitCode == 0 else {
407+
throw NSError(
408+
domain: "Remote",
409+
code: Int(
410+
result.exitCode
411+
),
412+
userInfo: [NSLocalizedDescriptionKey: "Failed to execute git command"]
413+
)
414+
}
415+
416+
guard let line = result.stdout.split(separator: "\n").first else {
417+
throw NSError(
418+
domain: "Remote",
419+
code: 1,
420+
userInfo: [NSLocalizedDescriptionKey: "No output from git command"]
421+
)
422+
}
423+
424+
let parts = line.split(separator: "\t")
425+
guard parts.count == 2 else {
426+
throw NSError(
427+
domain: "Remote",
428+
code: 2,
429+
userInfo: [NSLocalizedDescriptionKey: "Unexpected output format"]
430+
)
431+
}
432+
433+
let commitHash = String(parts[0])
434+
let ref = String(parts[1])
435+
436+
return (commitHash, ref)
437+
}
358438
}

‎Sources/Version-Control/Base/Commands/Rev-Parse.swift

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,31 @@
5656
/// - Warning:
5757
/// This function assumes that the Git executable is available and accessible in the system's PATH.
5858
func getRepositoryType(directoryURL: URL) throws -> RepositoryType {
59-
if FileManager().directoryExistsAtPath(directoryURL.relativePath) {
59+
if FileManager().directoryExistsAtPath(
60+
directoryURL.relativePath
61+
) {
6062
return .missing
6163
}
6264

6365
do {
64-
let result = try GitShell().git(args: ["rev-parse", "--is-bare-repository", "--show-cdup"],
65-
path: directoryURL,
66-
name: #function,
67-
options: IGitExecutionOptions(
68-
successExitCodes: Set([0, 128])
69-
))
66+
let result = try GitShell().git(
67+
args: [
68+
"rev-parse",
69+
"--is-bare-repository",
70+
"--show-cdup"
71+
],
72+
path: directoryURL,
73+
name: #function,
74+
options: IGitExecutionOptions(
75+
successExitCodes: Set([0, 128])
76+
)
77+
)
7078

7179
if result.exitCode == 0 {
7280
let lines = result.stdout.components(separatedBy: "\n")
7381
if let isBare = lines.first, let cdup = lines.dropFirst().first {
74-
return isBare == "true" ? .bare : .regular(
82+
return isBare == "true" ? .bare :
83+
.regular(
7584
topLevelWorkingDirectory: resolve(
7685
basePath: directoryURL.relativePath,
7786
relativePath: cdup
@@ -84,7 +93,9 @@
8493
of: "fatal: detected dubious ownership in repository at '(.+)'",
8594
options: .regularExpression
8695
) {
87-
let unsafePath = String(result.stderr[unsafeMatch])
96+
let unsafePath = String(
97+
result.stderr[unsafeMatch]
98+
)
8899
return .unsafe(path: unsafePath)
89100
}
90101

@@ -97,7 +108,10 @@
97108
}
98109
}
99110

100-
internal func resolve(basePath: String, relativePath: String) -> String {
111+
internal func resolve(
112+
basePath: String,
113+
relativePath: String
114+
) -> String {
101115
// Check if the relativePath is already an absolute path
102116
if relativePath.hasPrefix("/") {
103117
return relativePath
@@ -108,4 +122,55 @@
108122
return NSString(string: basePath).appendingPathComponent(expandedPath)
109123
}
110124

125+
/// Retrieve the latest commit hash of the current branch in a Git repository.
126+
///
127+
/// This function executes the `git rev-parse HEAD` command to get the latest commit hash
128+
/// of the current branch in the specified Git repository.
129+
///
130+
/// - Parameter directoryURL: The URL of the Git repository.
131+
///
132+
/// - Returns: A string representing the latest commit hash of the current branch.
133+
///
134+
/// - Throws: An error if there is a problem accessing the Git repository or executing the Git command.
135+
///
136+
/// - Example:
137+
/// ```swift
138+
/// let repositoryPath = URL(fileURLWithPath: "/path/to/repo")
139+
///
140+
/// do {
141+
/// let commitHash = try getLatestCommitHash(directoryURL: repositoryPath)
142+
/// print("Latest commit hash: \(commitHash)")
143+
/// } catch {
144+
/// print("Error retrieving the latest commit hash: \(error.localizedDescription)")
145+
/// }
146+
/// ```
147+
///
148+
/// - Note:
149+
/// This function uses the `git rev-parse HEAD` command to retrieve the latest commit hash.
150+
///
151+
/// - Warning:
152+
/// This function assumes that the Git executable is available and accessible in the system's PATH.
153+
public func getLatestCommitHash(
154+
directoryURL: URL
155+
) throws -> String {
156+
let result = try GitShell().git(
157+
args: [
158+
"rev-parse",
159+
"HEAD"
160+
],
161+
path: directoryURL,
162+
name: #function
163+
)
164+
165+
guard result.exitCode == 0 else {
166+
throw NSError(
167+
domain: "RevParse",
168+
code: Int(result.exitCode),
169+
userInfo: [NSLocalizedDescriptionKey: "Failed to execute git command"]
170+
)
171+
}
172+
173+
return result.stdout
174+
.trimmingCharacters(in: .whitespacesAndNewlines)
175+
}
111176
}

0 commit comments

Comments
 (0)
Please sign in to comment.