1
1
/*
2
2
This source file is part of the Swift.org open source project
3
3
4
- Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4
+ Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
5
5
Licensed under Apache License v2.0 with Runtime Library Exception
6
6
7
7
See http://swift.org/LICENSE.txt for license information
@@ -12,21 +12,6 @@ import TSCBasic
12
12
import Dispatch
13
13
import TSCUtility
14
14
15
- public struct GitCloneError : Swift . Error , CustomStringConvertible {
16
-
17
- /// The repository that was being cloned.
18
- public let repository : String
19
-
20
- /// The process result.
21
- public let result : ProcessResult
22
-
23
- public var description : String {
24
- let stdout = ( try ? result. utf8Output ( ) ) ?? " "
25
- let stderr = ( try ? result. utf8stderrOutput ( ) ) ?? " "
26
- let output = ( stdout + stderr) . spm_chomp ( ) . spm_multilineIndent ( count: 4 )
27
- return " Failed to clone \( repository) : \n \( output) "
28
- }
29
- }
30
15
31
16
/// A `git` repository provider.
32
17
public class GitRepositoryProvider : RepositoryProvider {
@@ -38,6 +23,29 @@ public class GitRepositoryProvider: RepositoryProvider {
38
23
self . processSet = processSet
39
24
}
40
25
26
+ /// Private function to invoke the Git tool with its default environment and given set of arguments. The specified
27
+ /// failure message is used only in case of error. This function waits for the invocation to finish and returns the
28
+ /// output as a string.
29
+ @discardableResult
30
+ private func callGit( _ args: String ... , environment: [ String : String ] = Git . environment, failureMessage: String = " " , repository: RepositorySpecifier ) throws -> String {
31
+ let process = Process ( arguments: [ Git . tool] + args, environment: environment, outputRedirection: . collect)
32
+ let result : ProcessResult
33
+ do {
34
+ try processSet? . add ( process)
35
+ try process. launch ( )
36
+ result = try process. waitUntilExit ( )
37
+ }
38
+ catch {
39
+ // Handle a failure to even launch the Git tool by synthesizing a result that we can wrap an error around.
40
+ result = ProcessResult ( arguments: process. arguments, environment: process. environment,
41
+ exitStatus: . terminated( code: - 1 ) , output: . failure( error) , stderrOutput: . failure( error) )
42
+ }
43
+ guard result. exitStatus == . terminated( code: 0 ) else {
44
+ throw GitCloneError ( repository: repository, message: failureMessage, result: result)
45
+ }
46
+ return try result. utf8Output ( )
47
+ }
48
+
41
49
public func fetch( repository: RepositorySpecifier , to path: AbsolutePath ) throws {
42
50
// Perform a bare clone.
43
51
//
@@ -47,24 +55,9 @@ public class GitRepositoryProvider: RepositoryProvider {
47
55
48
56
precondition ( !localFileSystem. exists ( path) )
49
57
50
- // FIXME: We need infrastructure in this subsystem for reporting
51
- // status information.
52
-
53
- let process = Process (
54
- args: Git . tool, " clone " , " --mirror " , repository. url, path. pathString, environment: Git . environment)
55
- // Add to process set.
56
- try processSet? . add ( process)
57
-
58
- try process. launch ( )
59
- let result = try process. waitUntilExit ( )
60
-
61
- // Throw if cloning failed.
62
- guard result. exitStatus == . terminated( code: 0 ) else {
63
- throw GitCloneError (
64
- repository: repository. url,
65
- result: result
66
- )
67
- }
58
+ // FIXME: Ideally we should pass `--progress` here and report status regularly. We currently don't have callbacks for that.
59
+ try callGit ( " clone " , " --mirror " , repository. url, path. pathString,
60
+ failureMessage: " Failed to clone repository \( repository. url) " , repository: repository)
68
61
}
69
62
70
63
public func open( repository: RepositorySpecifier , at path: AbsolutePath ) -> Repository {
@@ -82,8 +75,8 @@ public class GitRepositoryProvider: RepositoryProvider {
82
75
// For editable clones, i.e. the user is expected to directly work on them, first we create
83
76
// a clone from our cache of repositories and then we replace the remote to the one originally
84
77
// present in the bare repository.
85
- try Process . checkNonZeroExit ( args :
86
- Git . tool , " clone " , " --no-checkout " , sourcePath . pathString , destinationPath . pathString )
78
+ try callGit ( " clone " , " --no-checkout " , sourcePath . pathString , destinationPath . pathString ,
79
+ failureMessage : " Failed to clone repository \( repository . url ) " , repository : repository )
87
80
// The default name of the remote.
88
81
let origin = " origin "
89
82
// In destination repo remove the remote which will be pointing to the source repo.
@@ -101,8 +94,8 @@ public class GitRepositoryProvider: RepositoryProvider {
101
94
// re-resolve such that the objects in this repository changed, we would
102
95
// only ever expect to get back a revision that remains present in the
103
96
// object storage.
104
- try Process . checkNonZeroExit ( args :
105
- Git . tool , " clone " , " --shared " , " --no-checkout " , sourcePath . pathString , destinationPath . pathString )
97
+ try callGit ( " clone " , " --shared " , " --no-checkout " , sourcePath . pathString , destinationPath . pathString ,
98
+ failureMessage : " Failed to clone repository \( repository . url ) " , repository : repository )
106
99
}
107
100
}
108
101
@@ -118,6 +111,33 @@ public class GitRepositoryProvider: RepositoryProvider {
118
111
}
119
112
}
120
113
114
+
115
+ public struct GitCloneError : Error , CustomStringConvertible , DiagnosticLocationProviding {
116
+ public let repository : RepositorySpecifier
117
+ public let message : String
118
+ public let result : ProcessResult
119
+
120
+ public struct Location : DiagnosticLocation {
121
+ public let repository : RepositorySpecifier
122
+ public var description : String {
123
+ return repository. url
124
+ }
125
+ }
126
+
127
+ public var diagnosticLocation : DiagnosticLocation ? {
128
+ return Location ( repository: repository)
129
+ }
130
+
131
+ public var description : String {
132
+ let stdout = ( try ? result. utf8Output ( ) ) ?? " "
133
+ let stderr = ( try ? result. utf8stderrOutput ( ) ) ?? " "
134
+ let output = ( stdout + stderr) . spm_chomp ( ) . spm_multilineIndent ( count: 4 )
135
+ return " \( message) : \n \( output) "
136
+ }
137
+ }
138
+
139
+
140
+
121
141
enum GitInterfaceError : Swift . Error {
122
142
/// This indicates a problem communicating with the `git` tool.
123
143
case malformedResponse( String )
@@ -135,7 +155,7 @@ enum GitInterfaceError: Swift.Error {
135
155
// abstract, and change the provider to just return an adaptor around this
136
156
// class.
137
157
//
138
- /// A basic `git` repository. This class is thread safe.
158
+ /// A basic Git repository in the local file system (almost always a clone of a remote). This class is thread safe.
139
159
public class GitRepository : Repository , WorkingCheckout {
140
160
/// A hash object.
141
161
public struct Hash : Hashable {
@@ -225,7 +245,7 @@ public class GitRepository: Repository, WorkingCheckout {
225
245
public let contents : [ Entry ]
226
246
}
227
247
228
- /// The path of the repository on disk .
248
+ /// The path of the repository in the local file system .
229
249
public let path : AbsolutePath
230
250
231
251
/// The (serial) queue to execute git cli on.
@@ -238,13 +258,34 @@ public class GitRepository: Repository, WorkingCheckout {
238
258
self . path = path
239
259
self . isWorkingRepo = isWorkingRepo
240
260
do {
241
- let isBareRepo = try Process . checkNonZeroExit (
242
- args: Git . tool, " -C " , path. pathString, " rev-parse " , " --is-bare-repository " ) . spm_chomp ( ) == " true "
261
+ let isBareRepo = try callGit ( " rev-parse " , " --is-bare-repository " ) . spm_chomp ( ) == " true "
243
262
assert ( isBareRepo != isWorkingRepo)
244
263
} catch {
245
264
// Ignore if we couldn't run popen for some reason.
246
265
}
247
266
}
267
+
268
+ /// Private function to invoke the Git tool with its default environment and given set of arguments, specifying the
269
+ /// path of the repository as the one to operate on. The specified failure message is used only in case of error.
270
+ /// This function waits for the invocation to finish and returns the output as a string.
271
+ @discardableResult
272
+ private func callGit( _ args: String ... , environment: [ String : String ] = Git . environment, failureMessage: String = " " ) throws -> String {
273
+ let process = Process ( arguments: [ Git . tool, " -C " , path. pathString] + args, environment: environment, outputRedirection: . collect)
274
+ let result : ProcessResult
275
+ do {
276
+ try process. launch ( )
277
+ result = try process. waitUntilExit ( )
278
+ }
279
+ catch {
280
+ // Handle a failure to even launch the Git tool by synthesizing a result that we can wrap an error around.
281
+ result = ProcessResult ( arguments: process. arguments, environment: process. environment,
282
+ exitStatus: . terminated( code: - 1 ) , output: . failure( error) , stderrOutput: . failure( error) )
283
+ }
284
+ guard result. exitStatus == . terminated( code: 0 ) else {
285
+ throw GitRepositoryError ( path: self . path, message: failureMessage, result: result)
286
+ }
287
+ return try result. utf8Output ( )
288
+ }
248
289
249
290
/// Changes URL for the remote.
250
291
///
@@ -253,8 +294,8 @@ public class GitRepository: Repository, WorkingCheckout {
253
294
/// - url: The new url of the remote.
254
295
public func setURL( remote: String , url: String ) throws {
255
296
try queue. sync {
256
- try Process . checkNonZeroExit (
257
- args : Git . tool , " -C " , path . pathString , " remote " , " set-url " , remote, url)
297
+ try callGit ( " remote " , " set-url " , remote , url ,
298
+ failureMessage : " Couldn’t set the URL of the remote ‘ \( remote ) ’ to ‘ \( url) ’ " )
258
299
return
259
300
}
260
301
}
@@ -265,13 +306,13 @@ public class GitRepository: Repository, WorkingCheckout {
265
306
public func remotes( ) throws -> [ ( name: String , url: String ) ] {
266
307
return try queue. sync {
267
308
// Get the remote names.
268
- let remoteNamesOutput = try Process . checkNonZeroExit (
269
- args : Git . tool , " -C " , path . pathString , " remote " ) . spm_chomp ( )
309
+ let remoteNamesOutput = try callGit ( " remote " ,
310
+ failureMessage : " Couldn’t get the list of remotes " ) . spm_chomp ( )
270
311
let remoteNames = remoteNamesOutput. split ( separator: " \n " ) . map ( String . init)
271
312
return try remoteNames. map ( { name in
272
313
// For each remote get the url.
273
- let url = try Process . checkNonZeroExit (
274
- args : Git . tool , " -C " , path . pathString , " config " , " --get " , " remote. \( name) .url " ) . spm_chomp ( )
314
+ let url = try callGit ( " config " , " --get " , " remote. \( name ) .url " ,
315
+ failureMessage : " Couldn’t get the URL of the remote ‘ \( name) ’ " ) . spm_chomp ( )
275
316
return ( name, url)
276
317
} )
277
318
}
@@ -297,8 +338,8 @@ public class GitRepository: Repository, WorkingCheckout {
297
338
/// Returns the tags present in repository.
298
339
private func getTags( ) -> [ String ] {
299
340
// FIXME: Error handling.
300
- let tagList = try ! Process . checkNonZeroExit (
301
- args : Git . tool , " -C " , path . pathString , " tag " , " -l " ) . spm_chomp ( )
341
+ let tagList = try ! callGit ( " tag " , " -l " ,
342
+ failureMessage : " Couldn’t get the list of tags " ) . spm_chomp ( )
302
343
return tagList. split ( separator: " \n " ) . map ( String . init)
303
344
}
304
345
@@ -312,20 +353,17 @@ public class GitRepository: Repository, WorkingCheckout {
312
353
313
354
public func fetch( ) throws {
314
355
try queue. sync {
315
- try Process . checkNonZeroExit (
316
- args : Git . tool , " -C " , path . pathString , " remote " , " update " , " -p " , environment : Git . environment )
356
+ try callGit ( " remote " , " update " , " -p " ,
357
+ failureMessage : " Couldn’t fetch updates from remote repositories " )
317
358
self . tagsCache = nil
318
359
}
319
360
}
320
361
321
362
public func hasUncommittedChanges( ) -> Bool {
322
- // Only a work tree can have changes.
363
+ // Only a working repository can have changes.
323
364
guard isWorkingRepo else { return false }
324
365
return queue. sync {
325
- // Check nothing has been changed
326
- guard let result = try ? Process . checkNonZeroExit (
327
- args: Git . tool, " -C " , path. pathString, " status " , " -s " ) else
328
- {
366
+ guard let result = try ? callGit ( " status " , " -s " ) else {
329
367
return false
330
368
}
331
369
return !result. spm_chomp ( ) . isEmpty
@@ -340,26 +378,25 @@ public class GitRepository: Repository, WorkingCheckout {
340
378
341
379
public func hasUnpushedCommits( ) throws -> Bool {
342
380
return try queue. sync {
343
- let hasOutput = try Process . checkNonZeroExit (
344
- args : Git . tool , " -C " , path . pathString , " log " , " --branches " , " --not " , " --remotes " ) . spm_chomp ( ) . isEmpty
381
+ let hasOutput = try callGit ( " log " , " --branches " , " --not " , " --remotes " ,
382
+ failureMessage : " Couldn’t check for unpushed commits " ) . spm_chomp ( ) . isEmpty
345
383
return !hasOutput
346
384
}
347
385
}
348
386
349
387
public func getCurrentRevision( ) throws -> Revision {
350
388
return try queue. sync {
351
- return try Revision (
352
- identifier: Process . checkNonZeroExit (
353
- args: Git . tool, " -C " , path. pathString, " rev-parse " , " --verify " , " HEAD " ) . spm_chomp ( ) )
389
+ return try Revision ( identifier: callGit ( " rev-parse " , " --verify " , " HEAD " ,
390
+ failureMessage: " Couldn’t get current revision " ) . spm_chomp ( ) )
354
391
}
355
392
}
356
393
357
394
public func checkout( tag: String ) throws {
358
395
// FIXME: Audit behavior with off-branch tags in remote repositories, we
359
396
// may need to take a little more care here.
360
397
try queue. sync {
361
- try Process . checkNonZeroExit (
362
- args : Git . tool , " -C " , path . pathString , " reset " , " --hard " , tag)
398
+ try callGit ( " reset " , " --hard " , tag ,
399
+ failureMessage : " Couldn’t check out tag ‘ \( tag) ’ " )
363
400
try self . updateSubmoduleAndClean ( )
364
401
}
365
402
}
@@ -368,34 +405,32 @@ public class GitRepository: Repository, WorkingCheckout {
368
405
// FIXME: Audit behavior with off-branch tags in remote repositories, we
369
406
// may need to take a little more care here.
370
407
try queue. sync {
371
- try Process . checkNonZeroExit (
372
- args : Git . tool , " -C " , path . pathString , " checkout " , " -f " , revision. identifier)
408
+ try callGit ( " checkout " , " -f " , revision . identifier ,
409
+ failureMessage : " Couldn’t check out revision ‘ \( revision. identifier) ’ " )
373
410
try self . updateSubmoduleAndClean ( )
374
411
}
375
412
}
376
413
377
414
/// Initializes and updates the submodules, if any, and cleans left over the files and directories using git-clean.
378
415
private func updateSubmoduleAndClean( ) throws {
379
- try Process . checkNonZeroExit ( args : Git . tool ,
380
- " -C " , path . pathString , " submodule " , " update " , " --init " , " --recursive " , environment : Git . environment )
381
- try Process . checkNonZeroExit ( args : Git . tool ,
382
- " -C " , path . pathString , " clean " , " -ffdx " )
416
+ try callGit ( " submodule " , " update " , " --init " , " --recursive " ,
417
+ failureMessage : " Couldn’t update repository submodules " )
418
+ try callGit ( " clean " , " -ffdx " ,
419
+ failureMessage : " Couldn’t clean repository submodules " )
383
420
}
384
421
385
422
/// Returns true if a revision exists.
386
423
public func exists( revision: Revision ) -> Bool {
387
424
return queue. sync {
388
- let result = try ? Process . popen (
389
- args: Git . tool, " -C " , path. pathString, " rev-parse " , " --verify " , revision. identifier)
390
- return result? . exitStatus == . terminated( code: 0 )
425
+ return ( try ? callGit ( " rev-parse " , " --verify " , revision. identifier) ) != nil
391
426
}
392
427
}
393
428
394
429
public func checkout( newBranch: String ) throws {
395
- precondition ( isWorkingRepo, " This operation should run in a working repo. " )
430
+ precondition ( isWorkingRepo, " This operation is only value in a working repository " )
396
431
try queue. sync {
397
- try Process . checkNonZeroExit (
398
- args : Git . tool , " -C " , path . pathString , " checkout " , " -b " , newBranch)
432
+ try callGit ( " checkout " , " -b " , newBranch ,
433
+ failureMessage : " Couldn’t check out new branch ‘ \( newBranch) ’ " )
399
434
return
400
435
}
401
436
}
@@ -459,8 +494,8 @@ public class GitRepository: Repository, WorkingCheckout {
459
494
}
460
495
let response = try queue. sync {
461
496
try cachedHashes. memo ( key: specifier) {
462
- try Process . checkNonZeroExit (
463
- args : Git . tool , " -C " , path . pathString , " rev-parse " , " --verify " , specifier) . spm_chomp ( )
497
+ try callGit ( " rev-parse " , " --verify " , specifier ,
498
+ failureMessage : " Couldn’t get revision ‘ \( specifier) ’ " ) . spm_chomp ( )
464
499
}
465
500
}
466
501
if let hash = Hash ( response) {
@@ -754,3 +789,28 @@ private class GitFileSystemView: FileSystem {
754
789
fatalError ( " will never be supported " )
755
790
}
756
791
}
792
+
793
+
794
+ public struct GitRepositoryError : Error , CustomStringConvertible , DiagnosticLocationProviding {
795
+ public let path : AbsolutePath
796
+ public let message : String
797
+ public let result : ProcessResult
798
+
799
+ public struct Location : DiagnosticLocation {
800
+ public let path : AbsolutePath
801
+ public var description : String {
802
+ return path. pathString
803
+ }
804
+ }
805
+
806
+ public var diagnosticLocation : DiagnosticLocation ? {
807
+ return Location ( path: path)
808
+ }
809
+
810
+ public var description : String {
811
+ let stdout = ( try ? result. utf8Output ( ) ) ?? " "
812
+ let stderr = ( try ? result. utf8stderrOutput ( ) ) ?? " "
813
+ let output = ( stdout + stderr) . spm_chomp ( ) . spm_multilineIndent ( count: 4 )
814
+ return " \( message) : \n \( output) "
815
+ }
816
+ }
0 commit comments