@@ -43,35 +43,58 @@ func (c *CopyCmd) Run() error {
4343
4444 c .logger .Debugf ("Copying component %q to %q" , src , dst )
4545
46- if err := copyDir (src , dst ); err != nil {
46+ // Preflight: refuse before writing anything if any target file would
47+ // collide with something already in the working directory. Without this,
48+ // a mid-walk collision could leave the working tree in a half-copied
49+ // state.
50+ if err := preflightCopy (src , dst ); err != nil {
4751 return err
4852 }
4953
50- c .result .workingDir = dst
51-
5254 configName := configFileForKind (c .component .Kind )
53- if configName == "" {
54- return nil
55+
56+ var (
57+ refs ValuesReferences
58+ hasRefs bool
59+ )
60+
61+ if configName != "" {
62+ refs , err = CollectValuesReferences (filepath .Join (src , configName ))
63+ if err != nil {
64+ return err
65+ }
66+
67+ hasRefs = ! refs .IsEmpty ()
68+
69+ // Also preflight the values stub destination so we can fail before
70+ // copying when a stub would be written but the destination has an
71+ // unrelated obstruction (e.g. it exists as a directory).
72+ if hasRefs {
73+ if err := preflightValuesStub (dst ); err != nil {
74+ return err
75+ }
76+ }
5577 }
5678
57- refs , err := CollectValuesReferences (filepath .Join (src , configName ))
58- if err != nil {
79+ if err := copyDir (src , dst ); err != nil {
5980 return err
6081 }
6182
62- if refs .IsEmpty () {
63- return nil
64- }
83+ result := copyResult {workingDir : dst }
6584
66- c .result .references = refs
85+ if hasRefs {
86+ result .references = refs
6787
68- written , err := WriteValuesStub (dst , refs )
69- if err != nil {
70- return err
88+ written , err := WriteValuesStub (dst , refs )
89+ if err != nil {
90+ return err
91+ }
92+
93+ result .valuesWritten = written
94+ result .valuesSkipped = ! written
7195 }
7296
73- c .result .valuesWritten = written
74- c .result .valuesSkipped = ! written
97+ c .result = result
7598
7699 return nil
77100}
@@ -82,8 +105,14 @@ func (c *CopyCmd) Result() copyResult {
82105 return c .result
83106}
84107
85- func (c * CopyCmd ) SetStdin (io.Reader ) {}
108+ // SetStdin is a no-op; CopyCmd does not interact with stdio and only
109+ // implements this method to satisfy the tea.ExecCommand interface.
110+ func (c * CopyCmd ) SetStdin (io.Reader ) {}
111+
112+ // SetStdout is a no-op; see SetStdin.
86113func (c * CopyCmd ) SetStdout (io.Writer ) {}
114+
115+ // SetStderr is a no-op; see SetStdin.
87116func (c * CopyCmd ) SetStderr (io.Writer ) {}
88117
89118// resolvePaths returns the absolute source directory (inside the cloned repo)
@@ -157,7 +186,69 @@ func copyDir(src, dst string) error {
157186 })
158187}
159188
160- func copyFile (src , dst string ) error {
189+ // preflightCopy walks src and returns an error if any non-skipped regular
190+ // file would land on a path that already exists in dst. This makes the copy
191+ // step all-or-nothing for the common collision case, so a half-populated
192+ // working directory cannot result from a mid-walk conflict.
193+ func preflightCopy (src , dst string ) error {
194+ return filepath .WalkDir (src , func (path string , d fs.DirEntry , walkErr error ) error {
195+ if walkErr != nil {
196+ return walkErr
197+ }
198+
199+ if d .IsDir () {
200+ if path != src && skipDuringCopy (d .Name ()) {
201+ return filepath .SkipDir
202+ }
203+
204+ return nil
205+ }
206+
207+ if ! d .Type ().IsRegular () {
208+ return nil
209+ }
210+
211+ rel , err := filepath .Rel (src , path )
212+ if err != nil {
213+ return errors .New (err )
214+ }
215+
216+ target := filepath .Join (dst , rel )
217+ if _ , err := os .Lstat (target ); err == nil {
218+ return errors .Errorf ("destination %q already exists; refusing to overwrite" , target )
219+ } else if ! os .IsNotExist (err ) {
220+ return errors .New (err )
221+ }
222+
223+ return nil
224+ })
225+ }
226+
227+ // preflightValuesStub returns an error if WriteValuesStub would fail at the
228+ // stub destination for any reason other than a pre-existing values file
229+ // (which it intentionally leaves alone).
230+ func preflightValuesStub (dst string ) error {
231+ stub := filepath .Join (dst , valuesFileName )
232+
233+ info , err := os .Lstat (stub )
234+ if err != nil {
235+ if os .IsNotExist (err ) {
236+ return nil
237+ }
238+
239+ return errors .New (err )
240+ }
241+
242+ // A regular file at the stub path is fine; WriteValuesStub will leave
243+ // it alone. Anything else (directory, symlink, irregular) blocks us.
244+ if info .Mode ().IsRegular () {
245+ return nil
246+ }
247+
248+ return errors .Errorf ("destination %q is not a regular file; refusing to overwrite" , stub )
249+ }
250+
251+ func copyFile (src , dst string ) (err error ) {
161252 in , err := os .Open (src )
162253 if err != nil {
163254 return errors .New (err )
0 commit comments