@@ -12,7 +12,6 @@ import (
1212 "github.com/coder/envbuilder/options"
1313
1414 giturls "github.com/chainguard-dev/git-urls"
15- "github.com/coder/envbuilder/log"
1615 "github.com/go-git/go-billy/v5"
1716 "github.com/go-git/go-git/v5"
1817 "github.com/go-git/go-git/v5/plumbing"
@@ -47,11 +46,12 @@ type CloneRepoOptions struct {
4746// be cloned again.
4847//
4948// The bool returned states whether the repository was cloned or not.
50- func CloneRepo (ctx context.Context , opts CloneRepoOptions ) (bool , error ) {
49+ func CloneRepo (ctx context.Context , logf func ( string , ... any ), opts CloneRepoOptions ) (bool , error ) {
5150 parsed , err := giturls .Parse (opts .RepoURL )
5251 if err != nil {
5352 return false , fmt .Errorf ("parse url %q: %w" , opts .RepoURL , err )
5453 }
54+ logf ("Parsed Git URL as %q" , parsed .Redacted ())
5555 if parsed .Hostname () == "dev.azure.com" {
5656 // Azure DevOps requires capabilities multi_ack / multi_ack_detailed,
5757 // which are not fully implemented and by default are included in
@@ -73,6 +73,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
7373 transport .UnsupportedCapabilities = []capability.Capability {
7474 capability .ThinPack ,
7575 }
76+ logf ("Workaround for Azure DevOps: marking thin-pack as unsupported" )
7677 }
7778
7879 err = opts .Storage .MkdirAll (opts .Path , 0o755 )
@@ -131,7 +132,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
131132// clone will not be performed.
132133//
133134// The bool returned states whether the repository was cloned or not.
134- func ShallowCloneRepo (ctx context.Context , opts CloneRepoOptions ) error {
135+ func ShallowCloneRepo (ctx context.Context , logf func ( string , ... any ), opts CloneRepoOptions ) error {
135136 opts .Depth = 1
136137 opts .SingleBranch = true
137138
@@ -150,7 +151,7 @@ func ShallowCloneRepo(ctx context.Context, opts CloneRepoOptions) error {
150151 }
151152 }
152153
153- cloned , err := CloneRepo (ctx , opts )
154+ cloned , err := CloneRepo (ctx , logf , opts )
154155 if err != nil {
155156 return err
156157 }
@@ -182,14 +183,14 @@ func ReadPrivateKey(path string) (gossh.Signer, error) {
182183
183184// LogHostKeyCallback is a HostKeyCallback that just logs host keys
184185// and does nothing else.
185- func LogHostKeyCallback (logger log. Func ) gossh.HostKeyCallback {
186+ func LogHostKeyCallback (logger func ( string , ... any ) ) gossh.HostKeyCallback {
186187 return func (hostname string , remote net.Addr , key gossh.PublicKey ) error {
187188 var sb strings.Builder
188189 _ = knownhosts .WriteKnownHost (& sb , hostname , remote , key )
189190 // skeema/knownhosts uses a fake public key to determine the host key
190191 // algorithms. Ignore this one.
191192 if s := sb .String (); ! strings .Contains (s , "fake-public-key ZmFrZSBwdWJsaWMga2V5" ) {
192- logger (log . LevelInfo , "#1: 🔑 Got host key: %s" , strings .TrimSpace (s ))
193+ logger (" 🔑 Got host key: %s" , strings .TrimSpace (s ))
193194 }
194195 return nil
195196 }
@@ -203,6 +204,8 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
203204// | https?://host.tld/repo | Not Set | Set | HTTP Basic |
204205// | https?://host.tld/repo | Set | Not Set | HTTP Basic |
205206// | https?://host.tld/repo | Set | Set | HTTP Basic |
207+ // | file://path/to/repo | - | - | None |
208+ // | path/to/repo | - | - | None |
206209// | All other formats | - | - | SSH |
207210//
208211// For SSH authentication, the default username is "git" but will honour
@@ -214,58 +217,73 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
214217// If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured
215218// to accept and log all host keys. Otherwise, host key checking will be
216219// performed as usual.
217- func SetupRepoAuth (options * options.Options ) transport.AuthMethod {
220+ func SetupRepoAuth (logf func ( string , ... any ), options * options.Options ) transport.AuthMethod {
218221 if options .GitURL == "" {
219- options . Logger ( log . LevelInfo , "#1: ❔ No Git URL supplied!" )
222+ logf ( " ❔ No Git URL supplied!" )
220223 return nil
221224 }
222- if strings .HasPrefix (options .GitURL , "http://" ) || strings .HasPrefix (options .GitURL , "https://" ) {
225+ parsedURL , err := giturls .Parse (options .GitURL )
226+ if err != nil {
227+ logf ("❌ Failed to parse Git URL: %s" , err .Error ())
228+ return nil
229+ }
230+
231+ if parsedURL .Scheme == "http" || parsedURL .Scheme == "https" {
223232 // Special case: no auth
224233 if options .GitUsername == "" && options .GitPassword == "" {
225- options . Logger ( log . LevelInfo , "#1: 👤 Using no authentication!" )
234+ logf ( " 👤 Using no authentication!" )
226235 return nil
227236 }
228237 // Basic Auth
229238 // NOTE: we previously inserted the credentials into the repo URL.
230239 // This was removed in https://github.com/coder/envbuilder/pull/141
231- options . Logger ( log . LevelInfo , "#1: 🔒 Using HTTP basic authentication!" )
240+ logf ( " 🔒 Using HTTP basic authentication!" )
232241 return & githttp.BasicAuth {
233242 Username : options .GitUsername ,
234243 Password : options .GitPassword ,
235244 }
236245 }
237246
247+ if parsedURL .Scheme == "file" {
248+ // go-git will try to fallback to using the `git` command for local
249+ // filesystem clones. However, it's more likely than not that the
250+ // `git` command is not present in the container image. Log a warning
251+ // but continue. Also, no auth.
252+ logf ("🚧 Using local filesystem clone! This requires the git executable to be present!" )
253+ return nil
254+ }
255+
238256 // Generally git clones over SSH use the 'git' user, but respect
239257 // GIT_USERNAME if set.
240258 if options .GitUsername == "" {
241259 options .GitUsername = "git"
242260 }
243261
244262 // Assume SSH auth for all other formats.
245- options . Logger ( log . LevelInfo , "#1: 🔑 Using SSH authentication!" )
263+ logf ( " 🔑 Using SSH authentication!" )
246264
247265 var signer ssh.Signer
248266 if options .GitSSHPrivateKeyPath != "" {
249267 s , err := ReadPrivateKey (options .GitSSHPrivateKeyPath )
250268 if err != nil {
251- options . Logger ( log . LevelError , "#1: ❌ Failed to read private key from %s: %s" , options .GitSSHPrivateKeyPath , err .Error ())
269+ logf ( " ❌ Failed to read private key from %s: %s" , options .GitSSHPrivateKeyPath , err .Error ())
252270 } else {
253- options . Logger ( log . LevelInfo , "#1: 🔑 Using %s key!" , s .PublicKey ().Type ())
271+ logf ( " 🔑 Using %s key!" , s .PublicKey ().Type ())
254272 signer = s
255273 }
256274 }
257275
258276 // If no SSH key set, fall back to agent auth.
259277 if signer == nil {
260- options . Logger ( log . LevelError , "#1: 🔑 No SSH key found, falling back to agent!" )
278+ logf ( " 🔑 No SSH key found, falling back to agent!" )
261279 auth , err := gitssh .NewSSHAgentAuth (options .GitUsername )
262280 if err != nil {
263- options . Logger ( log . LevelError , "#1: ❌ Failed to connect to SSH agent: %s" , err .Error ())
281+ logf ( " ❌ Failed to connect to SSH agent: " + err .Error ())
264282 return nil // nothing else we can do
265283 }
266284 if os .Getenv ("SSH_KNOWN_HOSTS" ) == "" {
267- options . Logger ( log . LevelWarn , "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!" )
268- auth .HostKeyCallback = LogHostKeyCallback (options . Logger )
285+ logf ( " 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!" )
286+ auth .HostKeyCallback = LogHostKeyCallback (logf )
269287 }
270288 return auth
271289 }
@@ -283,19 +301,20 @@ func SetupRepoAuth(options *options.Options) transport.AuthMethod {
283301
284302 // Duplicated code due to Go's type system.
285303 if os .Getenv ("SSH_KNOWN_HOSTS" ) == "" {
286- options . Logger ( log . LevelWarn , "#1: 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!" )
287- auth .HostKeyCallback = LogHostKeyCallback (options . Logger )
304+ logf ( " 🔓 SSH_KNOWN_HOSTS not set, accepting all host keys!" )
305+ auth .HostKeyCallback = LogHostKeyCallback (logf )
288306 }
289307 return auth
290308}
291309
292- func CloneOptionsFromOptions (options options.Options ) (CloneRepoOptions , error ) {
310+ func CloneOptionsFromOptions (logf func ( string , ... any ), options options.Options ) (CloneRepoOptions , error ) {
293311 caBundle , err := options .CABundle ()
294312 if err != nil {
295313 return CloneRepoOptions {}, err
296314 }
297315
298316 cloneOpts := CloneRepoOptions {
317+ RepoURL : options .GitURL ,
299318 Path : options .WorkspaceFolder ,
300319 Storage : options .Filesystem ,
301320 Insecure : options .Insecure ,
@@ -304,13 +323,12 @@ func CloneOptionsFromOptions(options options.Options) (CloneRepoOptions, error)
304323 CABundle : caBundle ,
305324 }
306325
307- cloneOpts .RepoAuth = SetupRepoAuth (& options )
326+ cloneOpts .RepoAuth = SetupRepoAuth (logf , & options )
308327 if options .GitHTTPProxyURL != "" {
309328 cloneOpts .ProxyOptions = transport.ProxyOptions {
310329 URL : options .GitHTTPProxyURL ,
311330 }
312331 }
313- cloneOpts .RepoURL = options .GitURL
314332
315333 return cloneOpts , nil
316334}
@@ -331,7 +349,7 @@ func (w *progressWriter) Close() error {
331349 return err2
332350}
333351
334- func ProgressWriter (write func (line string )) io.WriteCloser {
352+ func ProgressWriter (write func (line string , args ... any )) io.WriteCloser {
335353 reader , writer := io .Pipe ()
336354 done := make (chan struct {})
337355 go func () {
@@ -347,6 +365,8 @@ func ProgressWriter(write func(line string)) io.WriteCloser {
347365 if line == "" {
348366 continue
349367 }
368+ // Escape % signs so that they don't get interpreted as format specifiers
369+ line = strings .Replace (line , "%" , "%%" , - 1 )
350370 write (strings .TrimSpace (line ))
351371 }
352372 }
0 commit comments