77use Illuminate \Console \Command ;
88use Illuminate \Support \Str ;
99use LaravelPlus \LaravelUpdater \Support \VersionControl ;
10+ use RuntimeException ;
1011use Throwable ;
1112
1213final class UpstreamUpgradeCommand extends Command
@@ -24,7 +25,9 @@ final class UpstreamUpgradeCommand extends Command
2425 {--remote-name=upstream : Remote name to use}
2526 {--pr-branch= : Override PR branch name}
2627 {--pr-title= : Override PR title}
27- {--pr-body= : Override PR body} ' ;
28+ {--pr-body= : Override PR body}
29+ {--force-conflicts : Force PR creation even with merge conflicts}
30+ {--force-push : Force push the PR branch even if conflicts exist} ' ;
2831
2932 protected $ description = 'Upgrade your project with the latest changes from upstream repository (creates PR by default). ' ;
3033
@@ -119,7 +122,7 @@ private function hasUpdates(array $cfg): bool
119122 private function createPullRequest (): int
120123 {
121124 $ this ->info ('🚀 Creating pull request with upstream updates... ' );
122-
125+
123126 $ cfg = config ('upstream ' , []);
124127 $ cfg = $ this ->applyDefaultPreset ($ cfg );
125128
@@ -151,6 +154,7 @@ private function createPullRequest(): int
151154
152155 if (! in_array ($ strategy , ['merge ' , 'rebase ' ], true )) {
153156 $ this ->error ('Invalid strategy. Use merge|rebase. ' );
157+
154158 return self ::FAILURE ;
155159 }
156160
@@ -168,11 +172,11 @@ private function createPullRequest(): int
168172
169173 // Show repository information
170174 $ this ->showRepositoryInfo ($ git , $ upstreamUrl );
171-
175+
172176 $ this ->line ("📝 PR Branch: <info> $ prBranch</info> " );
173177 $ this ->line ("🎯 Target Branch: <info> $ localBranch</info> " );
174178 $ this ->line ("📋 PR Title: <info> $ prTitle</info> " );
175-
179+
176180 if ($ dry ) {
177181 $ this ->newLine ();
178182 $ this ->line ('🔍 <comment>What will happen:</comment> ' );
@@ -184,7 +188,7 @@ private function createPullRequest(): int
184188 $ this ->line (' 6. 🎯 Create GitHub pull request ' );
185189 $ this ->newLine ();
186190 }
187-
191+
188192 $ this ->newLine ();
189193
190194 // Ensure remote exists
@@ -206,29 +210,88 @@ private function createPullRequest(): int
206210 // Create and checkout PR branch
207211 $ run ('create PR branch ' , [$ cfg ['git_binary ' ], 'checkout ' , '-b ' , $ prBranch ]);
208212
209- // Merge or rebase
213+ // Merge or rebase with conflict handling
210214 if ($ strategy === 'merge ' ) {
211215 $ args = [$ cfg ['git_binary ' ], 'merge ' , "$ remoteName/ $ upstreamBranch " , '--no-edit ' ];
212216 if ($ allowUnrelated ) {
213217 $ args [] = '--allow-unrelated-histories ' ;
214218 }
215- $ run ('merge ' , $ args );
219+
220+ try {
221+ $ run ('merge ' , $ args );
222+ } catch (RuntimeException $ e ) {
223+ // Handle merge conflicts by creating PR with conflicts
224+ $ this ->warn ('⚠️ Merge conflicts detected. Creating PR with conflicts for manual resolution on GitHub. ' );
225+ $ this ->line ('💡 You can resolve conflicts directly on GitHub or locally and push updates. ' );
226+
227+ // Check if there are conflicts
228+ $ status = $ git ->run (['status ' , '--porcelain ' ], throw: false );
229+ if (str_contains ($ status , 'UU ' ) || str_contains ($ status , 'AA ' ) || str_contains ($ status , 'DD ' )) {
230+ $ this ->line ('🔧 Conflicts found in: ' );
231+ $ conflictFiles = array_filter (explode ("\n" , $ status ), function ($ line ) {
232+ return str_contains ($ line , 'UU ' ) || str_contains ($ line , 'AA ' ) || str_contains ($ line , 'DD ' );
233+ });
234+ foreach ($ conflictFiles as $ file ) {
235+ $ this ->line (' - ' .mb_trim (mb_substr ($ file , 3 )));
236+ }
237+ }
238+
239+ // Continue with PR creation even with conflicts
240+ }
216241 } else {
217- $ run ('rebase ' , [$ cfg ['git_binary ' ], 'rebase ' , "$ remoteName/ $ upstreamBranch " ]);
242+ try {
243+ $ run ('rebase ' , [$ cfg ['git_binary ' ], 'rebase ' , "$ remoteName/ $ upstreamBranch " ]);
244+ } catch (RuntimeException $ e ) {
245+ // Handle rebase conflicts
246+ $ this ->warn ('⚠️ Rebase conflicts detected. Creating PR with conflicts for manual resolution on GitHub. ' );
247+ $ this ->line ('💡 You can resolve conflicts directly on GitHub or locally and push updates. ' );
248+
249+ // Abort the rebase to get back to a clean state
250+ try {
251+ $ git ->run (['rebase ' , '--abort ' ], throw: false );
252+ } catch (Throwable ) {
253+ // Ignore abort errors
254+ }
255+
256+ // Continue with PR creation
257+ }
218258 }
219259
220- // Commit (only if there are changes to commit )
260+ // Commit (always commit, even with conflicts )
221261 try {
222262 $ status = $ git ->run (['status ' , '--porcelain ' ], throw: false );
263+ $ hasConflicts = str_contains ($ status , 'UU ' ) || str_contains ($ status , 'AA ' ) || str_contains ($ status , 'DD ' );
264+
223265 if (! in_array (mb_trim ($ status ), ['' , '0 ' ], true )) {
224- $ run ('commit (ensure clean msg) ' , [$ cfg ['git_binary ' ], 'commit ' , '--no-edit ' , '-m ' , $ cfg ['commit_message ' ]]);
266+ if ($ hasConflicts ) {
267+ $ commitMessage = $ cfg ['commit_message ' ].' (with merge conflicts) ' ;
268+ $ this ->line ('📝 Committing changes with merge conflicts for PR creation... ' );
269+ } else {
270+ $ commitMessage = $ cfg ['commit_message ' ];
271+ }
272+ $ run ('commit (ensure clean msg) ' , [$ cfg ['git_binary ' ], 'commit ' , '--no-edit ' , '-m ' , $ commitMessage ]);
225273 }
226274 } catch (Throwable ) {
227275 // Ignore commit errors - might be nothing to commit
228276 }
229277
230- // Push PR branch
231- $ run ('push PR branch ' , [$ cfg ['git_binary ' ], 'push ' , 'origin ' , $ prBranch ]);
278+ // Push PR branch (force push if requested or if conflicts exist)
279+ $ status = $ git ->run (['status ' , '--porcelain ' ], throw: false );
280+ $ hasConflicts = str_contains ($ status , 'UU ' ) || str_contains ($ status , 'AA ' ) || str_contains ($ status , 'DD ' );
281+ $ forcePush = $ this ->option ('force-push ' ) || $ hasConflicts ;
282+
283+ if ($ forcePush ) {
284+ $ this ->line ('🚀 Force pushing PR branch (conflicts detected or --force-push specified)... ' );
285+ $ run ('force push PR branch ' , [$ cfg ['git_binary ' ], 'push ' , '--force ' , 'origin ' , $ prBranch ]);
286+ } else {
287+ try {
288+ $ run ('push PR branch ' , [$ cfg ['git_binary ' ], 'push ' , 'origin ' , $ prBranch ]);
289+ } catch (Throwable $ e ) {
290+ // If normal push fails, try force push
291+ $ this ->warn ('⚠️ Normal push failed, trying force push... ' );
292+ $ run ('force push PR branch ' , [$ cfg ['git_binary ' ], 'push ' , '--force ' , 'origin ' , $ prBranch ]);
293+ }
294+ }
232295
233296 // Post hooks
234297 if (! $ this ->option ('no-post ' )) {
@@ -238,6 +301,23 @@ private function createPullRequest(): int
238301 // Create GitHub PR
239302 $ prCreated = null ;
240303 if (! $ dry ) {
304+ // Check for conflicts and update PR body
305+ $ status = $ git ->run (['status ' , '--porcelain ' ], throw: false );
306+ $ hasConflicts = str_contains ($ status , 'UU ' ) || str_contains ($ status , 'AA ' ) || str_contains ($ status , 'DD ' );
307+
308+ if ($ hasConflicts ) {
309+ $ conflictFiles = array_filter (explode ("\n" , $ status ), function ($ line ) {
310+ return str_contains ($ line , 'UU ' ) || str_contains ($ line , 'AA ' ) || str_contains ($ line , 'DD ' );
311+ });
312+
313+ $ conflictList = '' ;
314+ foreach ($ conflictFiles as $ file ) {
315+ $ conflictList .= '- ' .mb_trim (mb_substr ($ file , 3 ))."\n" ;
316+ }
317+
318+ $ prBody .= "\n\n## ⚠️ Merge Conflicts Detected \n\nThis PR contains merge conflicts that need to be resolved: \n\n" .$ conflictList ."\nPlease resolve these conflicts before merging. " ;
319+ }
320+
241321 $ prCreated = $ this ->createGitHubPr ($ cfg , $ prBranch , $ localBranch , $ prTitle , $ prBody , $ upstreamUrl );
242322 }
243323
@@ -247,6 +327,16 @@ private function createPullRequest(): int
247327 $ this ->line ('💡 Run without <comment>--dry-run</comment> to execute the pull request creation. ' );
248328 } elseif ($ prCreated === true ) {
249329 $ this ->info ('✅ Pull request created successfully! ' );
330+
331+ // Check if there were conflicts
332+ $ status = $ git ->run (['status ' , '--porcelain ' ], throw: false );
333+ $ hasConflicts = str_contains ($ status , 'UU ' ) || str_contains ($ status , 'AA ' ) || str_contains ($ status , 'DD ' );
334+
335+ if ($ hasConflicts ) {
336+ $ this ->newLine ();
337+ $ this ->warn ('⚠️ This PR contains merge conflicts that need to be resolved on GitHub. ' );
338+ $ this ->line ('💡 You can resolve conflicts directly on GitHub or locally and push updates. ' );
339+ }
250340 } else {
251341 // Error details already printed inside createGitHubPr()
252342 return self ::FAILURE ;
@@ -258,7 +348,7 @@ private function createPullRequest(): int
258348 private function directUpgrade (): int
259349 {
260350 $ this ->info ('🚀 Upgrading with upstream updates... ' );
261-
351+
262352 // Build command arguments for sync command
263353 $ arguments = [];
264354 if ($ this ->option ('dry-run ' )) {
@@ -451,15 +541,15 @@ private function showRepositoryInfo(VersionControl $git, string $upstreamUrl): v
451541 try {
452542 // Get local repository URL
453543 $ localUrl = $ git ->run (['remote ' , 'get-url ' , 'origin ' ], throw: false );
454- if (!$ localUrl ) {
544+ if (! $ localUrl ) {
455545 $ localUrl = 'Local repository (no origin remote) ' ;
456546 }
457-
547+
458548 $ this ->line ('🏠 <comment>Repository Information:</comment> ' );
459549 $ this ->line (" Local: <info> $ localUrl</info> " );
460550 $ this ->line (" Upstream: <info> $ upstreamUrl</info> " );
461551 $ this ->newLine ();
462-
552+
463553 } catch (Throwable ) {
464554 // If we can't get repository info, just continue silently
465555 }
@@ -487,6 +577,7 @@ private function createGitHubPr(array $cfg, string $prBranch, string $targetBran
487577 if (! $ token || ! $ owner || ! $ repo ) {
488578 $ this ->warn ('⚠️ GitHub credentials not configured. Please set GITHUB_TOKEN, GITHUB_OWNER, and GITHUB_REPO in your .env file. ' );
489579 $ this ->line ("🔗 You can manually create a PR at: https://github.com/ $ owner/ $ repo/compare/ $ targetBranch... $ prBranch " );
580+
490581 return false ;
491582 }
492583
@@ -505,6 +596,7 @@ private function createGitHubPr(array $cfg, string $prBranch, string $targetBran
505596
506597 if ($ response && isset ($ response ['html_url ' ])) {
507598 $ this ->info ("✅ Pull request created: {$ response ['html_url ' ]}" );
599+
508600 return true ;
509601 }
510602
@@ -579,7 +671,7 @@ private function execHooks(string $key, bool $dry): void
579671 });
580672
581673 if (! $ process ->isSuccessful ()) {
582- throw new \ RuntimeException (in_array ($ process ->getErrorOutput (), ['' , '0 ' ], true ) ? $ process ->getOutput () : $ process ->getErrorOutput ());
674+ throw new RuntimeException (in_array ($ process ->getErrorOutput (), ['' , '0 ' ], true ) ? $ process ->getOutput () : $ process ->getErrorOutput ());
583675 }
584676 }
585677 }
0 commit comments