Skip to content

Commit 0e907f9

Browse files
Merge pull request #5 from runsascoded/rw/init
Improve `create` and `init` workflows
2 parents cca28a2 + 19bb74e commit 0e907f9

4 files changed

Lines changed: 240 additions & 70 deletions

File tree

src/ghpr/commands/create.py

Lines changed: 193 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def _parse_github_url(url: str) -> tuple[str | None, str | None]:
9696

9797

9898
def _read_and_parse_description() -> tuple[str, str]:
99-
"""Read DESCRIPTION.md and parse title and body.
99+
"""Read DESCRIPTION.md and parse title and body (expects plain format).
100100
101101
Returns:
102102
(title, body) tuple
@@ -105,31 +105,13 @@ def _read_and_parse_description() -> tuple[str, str]:
105105
err("Error: DESCRIPTION.md not found. Run 'ghpr init' first")
106106
exit(1)
107107

108-
with open('DESCRIPTION.md', 'r') as f:
109-
content = f.read()
110-
111-
lines = content.split('\n')
112-
if not lines:
113-
err("Error: DESCRIPTION.md is empty")
108+
# Read plain format (pre-creation)
109+
title, body = read_description_file(expect_plain=True)
110+
if not title:
111+
err("Error: Could not parse DESCRIPTION.md")
114112
exit(1)
115113

116-
# Parse title from first line
117-
first_line = lines[0].strip()
118-
if first_line.startswith('#'):
119-
title = first_line.lstrip('#').strip()
120-
# Remove any [owner/repo#NUM] prefix if present
121-
title = re.sub(r'^\[?[^/\]]+/[^#\]]+#\d+]?\s*', '', title)
122-
title = re.sub(r'^[^/]+/[^#]+#\w+\s+', '', title) # Handle owner/repo#NUMBER format
123-
else:
124-
title = first_line
125-
126-
# Get body (rest of the file)
127-
body_lines = lines[1:]
128-
while body_lines and not body_lines[0].strip():
129-
body_lines = body_lines[1:]
130-
body = '\n'.join(body_lines).strip()
131-
132-
return title, body
114+
return title, body or ''
133115

134116

135117
def _finalize_created_item(
@@ -152,13 +134,13 @@ def _finalize_created_item(
152134
new_filename = f'{repo}#{number}.md'
153135
new_file = Path(new_filename)
154136

155-
# Read title and body from DESCRIPTION.md
156-
title, body = read_description_file()
137+
# Read title and body from DESCRIPTION.md (plain format)
138+
title, body = read_description_file(expect_plain=True)
157139
if not title:
158140
err("Warning: Could not parse title from DESCRIPTION.md")
159141
return
160142

161-
# Write using helper with proper link reference format
143+
# Write using helper with link-reference format
162144
write_description_with_link_ref(
163145
new_file,
164146
owner,
@@ -255,21 +237,57 @@ def init(
255237
proc.run('git', 'config', 'pr.base', base, log=None)
256238
err(f"Base branch: {base}")
257239

258-
# Create initial DESCRIPTION.md
240+
# Create initial DESCRIPTION.md (plain format - link-reference added after PR creation)
259241
with open('DESCRIPTION.md', 'w') as f:
260-
if repo:
261-
f.write(f"# {repo}#NUMBER Title\n\n")
262-
else:
263-
f.write("# owner/repo#NUMBER Title\n\n")
242+
f.write("# Title\n\n")
264243
f.write("Description of the PR...\n")
265244

266245
err("Created DESCRIPTION.md template")
267246

247+
# Create initial commit
248+
proc.run('git', 'add', 'DESCRIPTION.md', log=None)
249+
proc.run('git', 'commit', '-m', 'Initial PR draft', log=None)
250+
err("Created initial commit")
251+
252+
# Create and configure gist mirror
253+
from ..gist import create_gist
254+
try:
255+
# Create gist (public by default, matching typical repo visibility)
256+
description = f'Draft PR for {owner}/{repo_name}' if owner and repo_name else 'Draft PR'
257+
gist_id = create_gist(
258+
file_path='DESCRIPTION.md',
259+
description=description,
260+
is_public=True,
261+
store_id=True # Automatically stores in git config pr.gist
262+
)
263+
264+
if gist_id:
265+
# Add gist as remote
266+
gist_url = f'git@gist.github.com:{gist_id}.git'
267+
proc.run('git', 'remote', 'add', 'g', gist_url, log=None)
268+
proc.run('git', 'config', 'pr.gist-remote', 'g', log=None)
269+
err(f"Created gist: https://gist.github.com/{gist_id}")
270+
err("Added remote 'g' for gist mirror")
271+
272+
# Fetch and set up tracking
273+
proc.run('git', 'fetch', 'g', log=None)
274+
proc.run('git', 'branch', '--set-upstream-to=g/main', 'main', log=None)
275+
err("Configured main branch to track g/main")
276+
277+
# Push initial commit to gist (with history)
278+
proc.run('git', 'push', '-f', 'g', 'main', log=None)
279+
err("Pushed initial commit to gist")
280+
except Exception as e:
281+
err(f"Warning: Could not create gist mirror: {e}")
282+
err("You can add it later with 'ghpr push -g'")
283+
284+
# End of cd(new_dir) context - we're done working in gh/new/
285+
268286
err("")
269287
err("Next steps:")
270288
err(" cd gh/new")
271289
err(" vim DESCRIPTION.md # Edit title and description")
272-
err(" git add DESCRIPTION.md && git commit -m 'Draft PR'")
290+
err(" git commit -am 'Update PR description'")
273291
err(" ghpr create")
274292

275293

@@ -279,25 +297,31 @@ def create(
279297
draft: bool,
280298
issue: bool,
281299
repo: str | None,
282-
web: bool,
300+
yes: int,
283301
dry_run: bool,
284302
) -> None:
285303
"""Create a new PR or Issue from the current draft."""
286304
if issue:
287-
create_new_issue(repo, web, dry_run)
305+
create_new_issue(repo, yes, dry_run)
288306
else:
289-
create_new_pr(head, base, draft, repo, web, dry_run)
307+
create_new_pr(head, base, draft, repo, yes, dry_run)
290308

291309

292310
def create_new_pr(
293311
head: str | None,
294312
base: str | None,
295313
draft: bool,
296314
repo_arg: str | None,
297-
web: bool,
315+
yes: int,
298316
dry_run: bool,
299317
) -> None:
300-
"""Create a new PR from DESCRIPTION.md."""
318+
"""Create a new PR from DESCRIPTION.md.
319+
320+
Args:
321+
yes: 0 = open web editor during creation (default)
322+
1 = skip prompt, create then open result
323+
2+ = skip all, create silently
324+
"""
301325
# Read and parse DESCRIPTION.md
302326
title, body = _read_and_parse_description()
303327

@@ -343,28 +367,41 @@ def create_new_pr(
343367

344368
# Get head branch - try to auto-detect from parent repo
345369
if not head:
370+
# Go up one level (to get out of gh/new/) and find the git repo root
346371
parent_dir = Path('..').resolve()
347-
if exists(join(parent_dir, '.git')):
348-
try:
349-
with cd(parent_dir):
350-
# Use branch resolution
372+
try:
373+
with cd(parent_dir):
374+
# Find the git repo root
375+
repo_root = proc.line('git', 'rev-parse', '--show-toplevel', err_ok=True, log=None)
376+
if not repo_root:
377+
err("Error: Could not detect head branch. Specify --head explicitly")
378+
exit(1)
379+
380+
with cd(repo_root):
381+
# Use branch resolution to get remote tracking branch
351382
ref_name, remote_ref = resolve_remote_ref(verbose=False)
352-
if ref_name:
383+
if remote_ref:
384+
# Extract branch name from remote_ref (e.g., "m/rw/ws3" -> "rw/ws3")
385+
# GitHub expects just the branch name without the remote prefix
386+
if '/' in remote_ref:
387+
head = '/'.join(remote_ref.split('/')[1:])
388+
else:
389+
head = remote_ref
390+
err(f"Auto-detected head branch from remote: {head}")
391+
elif ref_name:
392+
# Fallback to local branch name if no remote tracking
353393
head = ref_name
354-
err(f"Auto-detected head branch: {head}")
394+
err(f"Auto-detected head branch (local): {head}")
355395

356396
if not head:
357397
# Fallback to current branch
358398
head = proc.line('git', 'rev-parse', '--abbrev-ref', 'HEAD', log=None)
359399
if head == 'HEAD':
360400
err("Error: Parent repo is in detached HEAD state. Specify --head explicitly")
361401
exit(1)
362-
except Exception as e:
363-
err(f"Error detecting head branch: {e}")
364-
err("Specify --head explicitly")
365-
exit(1)
366-
else:
367-
err("Error: Could not detect head branch. Specify --head explicitly")
402+
except Exception as e:
403+
err(f"Error detecting head branch: {e}")
404+
err("Specify --head explicitly")
368405
exit(1)
369406

370407
# Create the PR
@@ -392,12 +429,60 @@ def create_new_pr(
392429
if draft:
393430
cmd.append('--draft')
394431

395-
if web:
432+
# Determine opening behavior based on yes count
433+
use_web_editor = (yes == 0)
434+
open_after = (yes == 1)
435+
436+
if use_web_editor:
396437
cmd.append('--web')
397438

398439
try:
399440
output = proc.text(*cmd, log=None).strip()
400-
if not web:
441+
442+
if use_web_editor:
443+
# Web editor mode: wait for user to finish editing in browser
444+
err("Opened PR in web editor")
445+
err("Press Enter when you've finished creating the PR in the browser...")
446+
input()
447+
448+
# Query GitHub to find the newly created PR
449+
err("Fetching PR information...")
450+
try:
451+
# List PRs for the head branch
452+
prs = proc.json('gh', 'pr', 'list', '-R', f'{owner}/{repo}', '--head', head, '--json', 'number,url', log=None)
453+
if prs and len(prs) > 0:
454+
pr_number = str(prs[0]['number'])
455+
pr_url = prs[0]['url']
456+
457+
# Store PR info in git config
458+
proc.run('git', 'config', 'pr.number', pr_number, log=None)
459+
proc.run('git', 'config', 'pr.url', pr_url, log=None)
460+
err(f"Found PR #{pr_number}: {pr_url}")
461+
462+
# Check for gist remote and store its ID if found
463+
try:
464+
remotes = proc.lines('git', 'remote', '-v', log=None)
465+
for remote_line in remotes:
466+
if 'gist.github.com' in remote_line:
467+
gist_match = GIST_ID_PATTERN.search(remote_line)
468+
if gist_match:
469+
gist_id = gist_match.group(1)
470+
proc.run('git', 'config', 'pr.gist', gist_id, log=None)
471+
err(f"Detected and stored gist ID: {gist_id}")
472+
break
473+
except Exception:
474+
pass
475+
476+
# Finalize: rename file, commit, rename directory
477+
_finalize_created_item(owner, repo, pr_number, pr_url, 'pr')
478+
else:
479+
err("Warning: Could not find newly created PR")
480+
err("You may need to run 'ghpr pull' to sync with GitHub")
481+
except Exception as e:
482+
err(f"Error: Could not fetch PR information: {e}")
483+
raise
484+
else:
485+
# API mode: PR number returned immediately
401486
# Extract PR number from URL
402487
match = re.search(r'/pull/(\d+)', output)
403488
if match:
@@ -408,6 +493,10 @@ def create_new_pr(
408493
err(f"Created PR #{pr_number}: {output}")
409494
err("PR info stored in git config")
410495

496+
# Open PR in browser if requested
497+
if open_after:
498+
proc.run('gh', 'pr', 'view', pr_number, '--web', '-R', f'{owner}/{repo}', log=None)
499+
411500
# Check for gist remote and store its ID if found
412501
try:
413502
remotes = proc.lines('git', 'remote', '-v', log=None)
@@ -435,10 +524,16 @@ def create_new_pr(
435524

436525
def create_new_issue(
437526
repo_arg: str | None,
438-
web: bool,
527+
yes: int,
439528
dry_run: bool,
440529
) -> None:
441-
"""Create a new Issue from DESCRIPTION.md."""
530+
"""Create a new Issue from DESCRIPTION.md.
531+
532+
Args:
533+
yes: 0 = open web editor during creation (default)
534+
1 = skip prompt, create then open result
535+
2+ = skip all, create silently
536+
"""
442537
# Read and parse DESCRIPTION.md
443538
title, body = _read_and_parse_description()
444539

@@ -461,12 +556,47 @@ def create_new_issue(
461556
'--title', title,
462557
'--body', body]
463558

464-
if web:
559+
# Determine opening behavior based on yes count
560+
use_web_editor = (yes == 0)
561+
open_after = (yes == 1)
562+
563+
if use_web_editor:
465564
cmd.append('--web')
466565

467566
try:
468567
output = proc.text(*cmd, log=None).strip()
469-
if not web:
568+
569+
if use_web_editor:
570+
# Web editor mode: wait for user to finish editing in browser
571+
err("Opened issue in web editor")
572+
err("Press Enter when you've finished creating the issue in the browser...")
573+
input()
574+
575+
# Query GitHub to find the newly created issue
576+
err("Fetching issue information...")
577+
try:
578+
# List recent issues in the repo and find the newest one
579+
issues = proc.json('gh', 'issue', 'list', '-R', f'{owner}/{repo}', '--json', 'number,url', '--limit', '1', log=None)
580+
if issues and len(issues) > 0:
581+
issue_number = str(issues[0]['number'])
582+
issue_url = issues[0]['url']
583+
584+
# Store issue info in git config
585+
proc.run('git', 'config', 'pr.number', issue_number, log=None)
586+
proc.run('git', 'config', 'pr.type', 'issue', log=None)
587+
proc.run('git', 'config', 'pr.url', issue_url, log=None)
588+
err(f"Found issue #{issue_number}: {issue_url}")
589+
590+
# Finalize: rename file, commit, rename directory
591+
_finalize_created_item(owner, repo, issue_number, issue_url, 'issue')
592+
else:
593+
err("Warning: Could not find newly created issue")
594+
err("You may need to run 'ghpr pull' to sync with GitHub")
595+
except Exception as e:
596+
err(f"Error: Could not fetch issue information: {e}")
597+
raise
598+
else:
599+
# API mode: issue number returned immediately
470600
# Extract issue number from URL
471601
match = re.search(r'/issues/(\d+)', output)
472602
if match:
@@ -478,10 +608,14 @@ def create_new_issue(
478608
err(f"Created issue #{issue_number}: {output}")
479609
err("Issue info stored in git config")
480610

611+
# Open issue in browser if requested
612+
if open_after:
613+
proc.run('gh', 'issue', 'view', issue_number, '--web', '-R', f'{owner}/{repo}', log=None)
614+
481615
# Finalize: rename file, commit, rename directory
482616
_finalize_created_item(owner, repo, issue_number, output, 'issue')
483-
else:
484-
err(f"Created issue: {output}")
617+
else:
618+
err(f"Created issue: {output}")
485619
except Exception as e:
486620
err(f"Error creating issue: {e}")
487621
exit(1)
@@ -506,7 +640,7 @@ def register(cli):
506640
flag('-i', '--issue', help='Create an issue instead of a PR')(
507641
flag('-n', '--dry-run', help='Show what would be done without creating')(
508642
opt('-r', '--repo', help='Repository (owner/repo format, default: auto-detect)')(
509-
flag('-w', '--web', help='Open in web browser after creating')(
643+
opt('-y', '--yes', count=True, default=0, help='Skip prompt: once = create then view, twice = create silently (default: open web editor during creation)')(
510644
create
511645
)
512646
)

0 commit comments

Comments
 (0)