Skip to content

Commit 8cabdd2

Browse files
author
nejc
committed
feat: ensure PR branch is always pushed, even with merge conflicts
- Always commit changes, even when merge conflicts exist - Add force push logic when conflicts are detected - Add --force-push option for explicit force pushing - Improve commit messages to indicate when conflicts exist - Ensure PR creation works reliably with conflicts
1 parent 83fe930 commit 8cabdd2

File tree

1 file changed

+109
-17
lines changed

1 file changed

+109
-17
lines changed

src/Console/Commands/UpstreamUpgradeCommand.php

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Console\Command;
88
use Illuminate\Support\Str;
99
use LaravelPlus\LaravelUpdater\Support\VersionControl;
10+
use RuntimeException;
1011
use Throwable;
1112

1213
final 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

Comments
 (0)