@@ -175,11 +175,12 @@ func applyCopyOperation(ctx context.Context,
175175
176176 if ! isGlobPattern {
177177 // Direct path reference - check what it actually is first (cp-like behavior)
178- return applySingleSourceCopy (ctx , op , srcRoot , srcPattern , stagingRoot , destRelPath , destEndsWithSlash )
178+ return applySingleSourceCopy (ctx , op , srcRoot , srcPattern , stagingRoot , stagingDir , destRelPath , destEndsWithSlash )
179179 }
180180
181181 // Glob pattern - find all matches and copy each
182- matches , err := fs .Glob (srcRoot .FS (), srcPattern )
182+ matches , err := doublestar .Glob (srcRoot .FS (), srcPattern )
183+
183184 if err != nil {
184185 return fmt .Errorf ("invalid glob pattern '%s': %w" , srcPattern , err )
185186 }
@@ -188,12 +189,19 @@ func applyCopyOperation(ctx context.Context,
188189 return fmt .Errorf ("no files match pattern '%s' in source '%s'" , srcPattern , srcAlias )
189190 }
190191
191- // Filter out excluded files
192+ // Filter out excluded files and special directory entries
192193 filteredMatches := make ([]string , 0 , len (matches ))
193194 for _ , match := range matches {
194- if ! shouldExclude (match , op .Exclude ) {
195- filteredMatches = append (filteredMatches , match )
195+ // Skip current directory and parent directory references
196+ // doublestar.Glob returns "." for patterns like "**" which would
197+ // cause the entire source to be copied, bypassing per-file strategies
198+ if match == "." || match == ".." {
199+ continue
200+ }
201+ if shouldExclude (match , op .Exclude ) {
202+ continue
196203 }
204+ filteredMatches = append (filteredMatches , match )
197205 }
198206
199207 if len (filteredMatches ) == 0 {
@@ -206,11 +214,25 @@ func applyCopyOperation(ctx context.Context,
206214 return err
207215 }
208216
209- // Calculate destination path based on glob pattern type
210- destFile := calculateGlobDestination (srcPattern , match , destRelPath )
211- if err := copyFileWithRoots (ctx , op , srcRoot , match , stagingRoot , destFile ); err != nil {
212- return fmt .Errorf ("failed to copy file '%s' to '%s': %w" , match , destFile , err )
217+ // Handle Extract strategy for tarballs
218+ if op .Strategy == swapi .ExtractStrategy {
219+ if ! isTarball (match ) {
220+ // Ignore files that are not tarball archives
221+ continue
222+ }
223+ if err := extractTarball (ctx , srcRoot , match , stagingDir , destRelPath ); err != nil {
224+ return fmt .Errorf ("failed to extract tarball '%s' to '%s': %w" , match , destRelPath , err )
225+ }
226+
227+ } else {
228+ // Calculate destination path based on glob pattern type
229+ destFile := calculateGlobDestination (srcPattern , match , destRelPath )
230+
231+ if err := copyFileWithRoots (ctx , op , srcRoot , match , stagingRoot , destFile ); err != nil {
232+ return fmt .Errorf ("failed to copy file '%s' to '%s': %w" , match , destFile , err )
233+ }
213234 }
235+
214236 }
215237
216238 return nil
@@ -223,6 +245,7 @@ func applySingleSourceCopy(ctx context.Context,
223245 srcRoot * os.Root ,
224246 srcPath string ,
225247 stagingRoot * os.Root ,
248+ stagingDir string ,
226249 destPath string ,
227250 destEndsWithSlash bool ) error {
228251 // Clean the source path to handle trailing slashes
@@ -238,10 +261,14 @@ func applySingleSourceCopy(ctx context.Context,
238261 }
239262
240263 if srcInfo .IsDir () {
264+ // Extract strategy is not supported for directories
265+ if op .Strategy == swapi .ExtractStrategy {
266+ return fmt .Errorf ("extract strategy is not supported for directories, got '%s'" , srcPath )
267+ }
241268 return applySingleDirectoryCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath )
242- } else {
243- return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath , destEndsWithSlash )
244269 }
270+
271+ return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , stagingDir , destPath , destEndsWithSlash )
245272}
246273
247274// applySingleFileCopy handles copying a single file using cp-like semantics:
@@ -252,12 +279,22 @@ func applySingleFileCopy(ctx context.Context,
252279 srcRoot * os.Root ,
253280 srcPath string ,
254281 stagingRoot * os.Root ,
282+ stagingDir string ,
255283 destPath string ,
256284 destEndsWithSlash bool ) error {
257285 // Check if the file should be excluded
258286 if shouldExclude (srcPath , op .Exclude ) {
259287 return nil // Skip excluded file
260288 }
289+
290+ // Handle Extract strategy for tarballs
291+ if op .Strategy == swapi .ExtractStrategy {
292+ if ! isTarball (srcPath ) {
293+ return fmt .Errorf ("extract strategy requires tarball file (.tar.gz or .tgz), got '%s'" , srcPath )
294+ }
295+ return extractTarball (ctx , srcRoot , srcPath , stagingDir , destPath )
296+ }
297+
261298 var finalDestPath string
262299
263300 if destEndsWithSlash {
@@ -302,7 +339,9 @@ func containsGlobChars(path string) bool {
302339// to match cp-like behavior for different glob patterns:
303340// - dir/** patterns strip the directory prefix (like cp -r dir/** dest/)
304341// - other patterns preserve the full match path
342+ // If stripFilename is true, only the directory structure is preserved (used for Extract strategy).
305343func calculateGlobDestination (pattern , match , destPath string ) string {
344+
306345 // Check if pattern ends with /** (recursive contents pattern)
307346 if strings .HasSuffix (pattern , "/**" ) {
308347 // Extract the directory prefix from pattern (everything before /**)
@@ -545,12 +584,21 @@ func shouldExclude(filePath string, excludePatterns []string) bool {
545584 return false
546585 }
547586
587+ fileName := filepath .Base (filePath )
588+
548589 for _ , pattern := range excludePatterns {
549590 // We validate the patterns when parsing the copy operation,
550591 // so it's safe to use MatchUnvalidated here.
551592 if doublestar .MatchUnvalidated (pattern , filePath ) {
552593 return true
553594 }
595+ // For simple patterns without path separators (e.g., "*.md"),
596+ // also match against just the filename. This provides a more
597+ // intuitive user experience where "*.md" excludes all markdown
598+ // files regardless of their directory depth.
599+ if ! strings .Contains (pattern , "/" ) && doublestar .MatchUnvalidated (pattern , fileName ) {
600+ return true
601+ }
554602 }
555603
556604 return false
0 commit comments