2828use Symfony \Component \Console \Input \InputInterface ;
2929use Symfony \Component \Console \Input \InputOption ;
3030use Symfony \Component \Console \Output \OutputInterface ;
31+ use Symfony \Component \Console \Question \ConfirmationQuestion ;
3132use Symfony \Component \Filesystem \Path ;
3233
3334/**
@@ -80,6 +81,21 @@ protected function configure(): void
8081 shortcut: 'o ' ,
8182 mode: InputOption::VALUE_NONE ,
8283 description: 'Overwrite existing target files. ' ,
84+ )
85+ ->addOption (
86+ name: 'dry-run ' ,
87+ mode: InputOption::VALUE_NONE ,
88+ description: 'Preview copied resources without writing files. ' ,
89+ )
90+ ->addOption (
91+ name: 'check ' ,
92+ mode: InputOption::VALUE_NONE ,
93+ description: 'Report copied-resource drift and exit non-zero when changes are required. ' ,
94+ )
95+ ->addOption (
96+ name: 'interactive ' ,
97+ mode: InputOption::VALUE_NONE ,
98+ description: 'Prompt before replacing drifted resources. ' ,
8399 );
84100 }
85101
@@ -96,6 +112,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
96112 $ source = (string ) $ input ->getOption ('source ' );
97113 $ target = (string ) $ input ->getOption ('target ' );
98114 $ overwrite = (bool ) $ input ->getOption ('overwrite ' );
115+ $ dryRun = (bool ) $ input ->getOption ('dry-run ' );
116+ $ check = (bool ) $ input ->getOption ('check ' );
117+ $ interactive = (bool ) $ input ->getOption ('interactive ' );
99118
100119 if ('' === $ source || '' === $ target ) {
101120 $ output ->writeln ('<error>The --source and --target options are required.</error> ' );
@@ -107,10 +126,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
107126 $ targetPath = (string ) $ this ->filesystem ->getAbsolutePath ($ target );
108127
109128 if (is_dir ($ sourcePath )) {
110- return $ this ->copyDirectory ($ sourcePath , $ targetPath , $ overwrite , $ output );
129+ return $ this ->copyDirectory ($ sourcePath , $ targetPath , $ overwrite , $ dryRun , $ check , $ interactive , $ input , $ output );
111130 }
112131
113- return $ this ->copyFile ($ sourcePath , $ targetPath , $ overwrite , $ output );
132+ return $ this ->copyFile ($ sourcePath , $ targetPath , $ overwrite , $ dryRun , $ check , $ interactive , $ input , $ output );
114133 }
115134
116135 /**
@@ -127,19 +146,28 @@ private function copyDirectory(
127146 string $ sourcePath ,
128147 string $ targetPath ,
129148 bool $ overwrite ,
149+ bool $ dryRun ,
150+ bool $ check ,
151+ bool $ interactive ,
152+ InputInterface $ input ,
130153 OutputInterface $ output
131154 ): int {
132155 $ files = $ this ->finderFactory
133156 ->create ()
134157 ->files ()
135158 ->in ($ sourcePath );
136159
160+ $ status = self ::SUCCESS ;
161+
137162 foreach ($ files as $ file ) {
138163 $ destination = Path::join ($ targetPath , $ file ->getRelativePathname ());
139- $ this ->copyFile ($ file ->getRealPath (), $ destination , $ overwrite , $ output );
164+ $ status = max (
165+ $ status ,
166+ $ this ->copyFile ($ file ->getRealPath (), $ destination , $ overwrite , $ dryRun , $ check , $ interactive , $ input , $ output ),
167+ );
140168 }
141169
142- return self :: SUCCESS ;
170+ return $ status ;
143171 }
144172
145173 /**
@@ -152,15 +180,24 @@ private function copyDirectory(
152180 *
153181 * @return int the command status code
154182 */
155- private function copyFile (string $ sourcePath , string $ targetPath , bool $ overwrite , OutputInterface $ output ): int
183+ private function copyFile (
184+ string $ sourcePath ,
185+ string $ targetPath ,
186+ bool $ overwrite ,
187+ bool $ dryRun ,
188+ bool $ check ,
189+ bool $ interactive ,
190+ InputInterface $ input ,
191+ OutputInterface $ output ,
192+ ): int
156193 {
157- if (! $ overwrite && $ this ->filesystem ->exists ($ targetPath )) {
194+ if (! $ overwrite && ! $ dryRun && ! $ check && ! $ interactive && $ this ->filesystem ->exists ($ targetPath )) {
158195 $ output ->writeln (\sprintf ('<comment>Skipped existing resource %s.</comment> ' , $ targetPath ));
159196
160197 return self ::SUCCESS ;
161198 }
162199
163- if ($ overwrite && $ this ->filesystem ->exists ($ targetPath )) {
200+ if (( $ overwrite || $ dryRun || $ check || $ interactive ) && $ this ->filesystem ->exists ($ targetPath )) {
164201 $ comparison = $ this ->overwriteDiffRenderer ->render ($ sourcePath , $ targetPath );
165202
166203 $ output ->writeln (\sprintf ('<comment>%s</comment> ' , $ comparison ->summary ()));
@@ -172,11 +209,41 @@ private function copyFile(string $sourcePath, string $targetPath, bool $overwrit
172209 if ($ comparison ->isUnchanged ()) {
173210 return self ::SUCCESS ;
174211 }
212+
213+ if ($ check ) {
214+ return self ::FAILURE ;
215+ }
216+
217+ if ($ dryRun ) {
218+ return self ::SUCCESS ;
219+ }
220+
221+ if ($ interactive && $ input ->isInteractive () && ! $ this ->shouldReplaceResource ($ input , $ output , $ targetPath )) {
222+ $ output ->writeln (\sprintf ('<comment>Skipped replacing %s.</comment> ' , $ targetPath ));
223+
224+ return self ::SUCCESS ;
225+ }
175226 }
176227
177- $ this ->filesystem ->copy ($ sourcePath , $ targetPath , $ overwrite );
228+ $ this ->filesystem ->copy ($ sourcePath , $ targetPath , $ overwrite || $ interactive );
178229 $ output ->writeln (\sprintf ('<info>Copied resource %s.</info> ' , $ targetPath ));
179230
180231 return self ::SUCCESS ;
181232 }
233+
234+ /**
235+ * Prompts whether a drifted resource should be replaced.
236+ *
237+ * @param InputInterface $input the command input
238+ * @param OutputInterface $output the command output
239+ * @param string $targetPath the resource path that would be replaced
240+ *
241+ * @return bool true when the replacement SHOULD proceed
242+ */
243+ private function shouldReplaceResource (InputInterface $ input , OutputInterface $ output , string $ targetPath ): bool
244+ {
245+ $ question = new ConfirmationQuestion (\sprintf ('Replace drifted resource %s? [y/N] ' , $ targetPath ), false );
246+
247+ return (bool ) $ this ->getHelper ('question ' )->ask ($ input , $ output , $ question );
248+ }
182249}
0 commit comments