-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathMerge.swift
243 lines (224 loc) · 9.98 KB
/
Merge.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
//
// Merge.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 enum MergeResult {
/// The merge completed successfully
case success
/// The merge was a noop since the current branch
/// was already up to date with the target branch.
case alreadyUpToDate
/// The merge failed, likely due to conflicts.
case failed
}
public struct Merge {
public init() {}
/// Merge the named branch into the current branch in the Git repository.
///
/// This function performs a merge operation,
/// incorporating changes from a named branch into the current branch of the Git repository. \
/// You can specify whether to perform a regular merge or a squash merge.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository.
/// - branch: The name of the branch to merge into the current branch.
/// - isSquash: A flag indicating whether to perform a squash merge. Default is `false`.
///
/// - Returns: A `MergeResult` indicating the result of the merge operation.
///
/// - Throws: An error if there was an issue with the merge operation or if the provided branch name is invalid.
///
/// - Example:
/// ```swift
/// let repositoryURL = URL(fileURLWithPath: "/path/to/git/repo")
/// let branchToMerge = "feature/branch-to-merge"
///
/// do {
/// let mergeResult = try merge(directoryURL: repositoryURL, branch: branchToMerge, isSquash: false)
/// switch mergeResult {
/// case .success:
/// print("Merge successful.")
/// case .alreadyUpToDate:
/// print("Branch is already up to date.")
/// }
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
///
/// - Important: This function performs a merge operation in the Git repository. \
/// Ensure that the provided branch name is valid and that the repository is in a \
/// clean state before calling this function.
public func merge(directoryURL: URL,
branch: String,
isSquash: Bool = false) throws -> MergeResult {
var args = ["merge"]
if isSquash {
args.append("--squash")
}
args.append(branch)
// Execute the Git merge command.
let result = try GitShell().git(args: args,
path: directoryURL,
name: #function,
options: IGitExecutionOptions(expectedErrors: Set([GitError.MergeConflicts])))
if result.exitCode != 0 {
return MergeResult.failed
}
// If squash merge was requested, commit the changes without editing.
if isSquash {
let exitCode = try GitShell().git(args: ["commit", "--no-edit"],
path: directoryURL,
name: #function)
if exitCode.exitCode != 0 {
return MergeResult.failed
}
}
return result.stdout == noopMergeMessage ? MergeResult.alreadyUpToDate : MergeResult.success
}
private let noopMergeMessage = "Already up to date.\n"
/// Find the base commit between two commit-ish identifiers in the Git repository.
///
/// This function calculates the merge base commit between two commit-ish identifiers
/// (e.g., branch names, commit hashes) in the Git repository.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository.
/// - firstCommitish: The first commit-ish identifier.
/// - secondCommitish: The second commit-ish identifier.
///
/// - Returns: The commit hash of the merge base if found, or `nil` if there is no common base commit.
///
/// - Throws: An error if there was an issue calculating the merge base or if the commit-sh \
/// identifiers are invalid.
///
/// - Example:
/// ```swift
/// let repositoryURL = URL(fileURLWithPath: "/path/to/git/repo")
/// let firstBranch = "feature/branch-a"
/// let secondBranch = "feature/branch-b"
/// if let mergeBase = try getMergeBase(
/// directoryURL: repositoryURL,
/// firstCommitish: firstBranch,
/// secondCommitish: secondBranch
/// ) {
/// print("Merge base commit: \(mergeBase)")
/// } else {
/// print("No common merge base found.")
/// }
/// ```
///
/// - Important: This function requires valid commit-ish identifiers as input, \
/// and it may return `nil` if there is no common merge base between the provided \
/// commit-ish identifiers.
public func getMergeBase(directoryURL: URL,
firstCommitish: String,
secondCommitish: String) throws -> String? {
let process = try GitShell().git(args: ["merge-base",
firstCommitish,
secondCommitish],
path: directoryURL,
name: #function,
options: IGitExecutionOptions(successExitCodes: Set([0, 1, 128])))
// - 1 is returned if a common ancestor cannot be resolved
// - 128 is returned if a ref cannot be found
// "warning: ignoring broken ref refs/remotes/origin/main."
if process.exitCode == 1 || process.exitCode == 128 {
return nil
}
return process.stdout.trimmingCharacters(in: .whitespaces)
}
/// Abort a conflicted merge in the Git repository.
///
/// This function aborts a mid-flight merge operation that is in a conflicted state. \
/// It is equivalent to running the `git merge --abort` command in the repository.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository.
///
/// - Throws: An error if there was an issue aborting the merge operation or \
/// if the repository is not in a merge-conflicted state.
///
/// - Example:
/// ```swift
/// let repositoryURL = URL(fileURLWithPath: "/path/to/git/repo")
/// try abortMerge(directoryURL: repositoryURL)
/// print("Merge aborted successfully.")
/// ```
///
/// - Important: This function should only be called when the repository is in a conflicted state \
/// due to a mid-flight merge operation.
public func abortMerge(directoryURL: URL) throws {
// Execute the `git merge --abort` command to abort the conflicted merge.
try GitShell().git(args: ["merge", "--abort"],
path: directoryURL,
name: #function)
}
/// Check if the `.git/MERGE_HEAD` file exists in the Git repository.
///
/// This function checks for the presence of the `.git/MERGE_HEAD` file in the repository's Git directory. \
/// The existence of this file typically indicates that the repository is in a conflicted state due to
/// an ongoing merge operation.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository.
///
/// - Returns: `true` if the `.git/MERGE_HEAD` file exists, indicating a conflicted state; otherwise, `false`.
///
/// - Throws: An error if there was an issue accessing or reading the repository's Git directory.
///
/// - Example:
/// ```swift
/// let repositoryURL = URL(fileURLWithPath: "/path/to/git/repo")
/// let isMergeHead = try isMergeHeadSet(directoryURL: repositoryURL)
/// if isMergeHead {
/// print("The repository is in a conflicted state due to an ongoing merge operation.")
/// } else {
/// print("The repository is not in a conflicted state.")
/// }
/// ```
///
/// - Returns: `true` if the `.git/MERGE_HEAD` file exists; otherwise, `false`.
public func isMergeHeadSet(directoryURL: URL) throws -> Bool {
let path = try String(contentsOf: directoryURL) + ".git/MERGE_HEAD"
return FileManager.default.fileExists(atPath: path)
}
/// Check if the `.git/SQUASH_MSG` file exists in the Git repository.
///
/// This function checks for the presence of the `.git/SQUASH_MSG` file in the repository's Git directory. \
/// The existence of this file typically indicates that a merge with the `--squash` option has been initiated,
/// and the merge has not yet been committed. It can be an indicator of a detected conflict.
///
/// - Parameters:
/// - directoryURL: The URL of the Git repository.
///
/// - Returns: `true` if the `.git/SQUASH_MSG` file exists, \
/// indicating a potential merge --squash scenario; otherwise, `false`.
///
/// - Throws: An error if there was an issue accessing or reading the repository's Git directory.
///
/// - Note: If a merge --squash is aborted, the `.git/SQUASH_MSG` file may not be cleared automatically, \
/// leading to its presence in non-merge --squashing scenarios.
///
/// - Example:
/// ```swift
/// let repositoryURL = URL(fileURLWithPath: "/path/to/git/repo")
/// let isSquashMsg = try isSquashMsgSet(directoryURL: repositoryURL)
/// if isSquashMsg {
/// print("Potential merge --squash scenario detected.")
/// } else {
/// print("No merge --squash scenario detected.")
/// }
/// ```
///
/// - Returns: `true` if the `.git/SQUASH_MSG` file exists; otherwise, `false`.
public func isSquashMsgSet(directoryURL: URL) throws -> Bool {
let path = try String(contentsOf: directoryURL) + ".git/SQUASH_MSG"
return FileManager.default.fileExists(atPath: path)
}
}