2222class MemberApp (App ):
2323 """Single app that toggles between a file list and a form while connected to a GitHub fork+push flow."""
2424
25- def __init__ (self , repo_path : str ) -> None :
25+ def __init__ (
26+ self ,
27+ original_repo : Repository ,
28+ forked_repo : Repository ,
29+ token : str ,
30+ repo_path : str ,
31+ ) -> None :
2632 super ().__init__ ()
33+ self .original_repo = original_repo
34+ self .forked_repo = forked_repo
35+ self .token = token
2736 self .repo_path = repo_path
2837
2938 def compose (self ) -> ComposeResult :
@@ -476,112 +485,18 @@ def save_member(self) -> None:
476485
477486 md_content = "\n " .join (md_lines )
478487
479- if not self .current_file :
480- # compute name_file
481- if aliases :
482- alias_for_name = aliases [0 ].lower ().replace (" " , "_" )
483- else :
484- alias_for_name = name .lower ().replace (" " , "_" )
485-
486- sha_hash = hashlib .sha1 (
487- (alias_for_name + email + datetime .now ().isoformat ()).encode (
488- "utf-8"
489- )
490- ).hexdigest ()[:8 ]
491- name_file = f"{ alias_for_name } -{ sha_hash } "
492-
493- # Write file
494- file_path = os .path .join (
495- self .repo_path , "blog" , "members" , f"{ name_file } .md"
496- )
497- else :
498- name_file = self .current_file
499- file_path = os .path .join (
500- self .repo_path , "blog" , "members" , f"{ name_file } "
501- )
502- os .makedirs (os .path .dirname (file_path ), exist_ok = True )
503- with open (file_path , "w" , encoding = "utf-8" ) as f :
504- f .write (md_content )
505-
506- # commit & push
507- repo = pygit2 .Repository (self .repo_path )
508- rel_path = os .path .relpath (file_path , self .repo_path )
509- rel_path = pathlib .Path (
510- rel_path
511- ).as_posix () # Force path to POSIX format so Windows backslashes (\) don't break pygit2
512- repo .index .add (rel_path )
513- repo .index .write ()
514- author_sig = pygit2 .Signature (
515- name or "Unknown" , email or "unknown@email"
516- )
517- tree_id = repo .index .write_tree ()
518- parents = [] if repo .head_is_unborn else [repo .head .target ]
519- commit_msg = (
520- f"Changed { self .current_file } "
521- if self .current_file
522- else f"Added { name_file } .md"
523- )
524- repo .create_commit (
525- "HEAD" , author_sig , author_sig , commit_msg , tree_id , parents
488+ message = create_pr (
489+ md_content ,
490+ self .current_file ,
491+ self .repo_path ,
492+ self .original_repo ,
493+ self .forked_repo ,
494+ self .token ,
495+ aliases ,
496+ name ,
497+ email ,
526498 )
527-
528- callbacks = pygit2 .callbacks .RemoteCallbacks (
529- credentials = pygit2 .UserPass (self .token , "x-oauth-basic" )
530- )
531- remote = repo .remotes ["origin" ]
532- remote .push ([repo .head .name ], callbacks = callbacks )
533-
534- # PR logic
535- pr_title = commit_msg
536- first_alias = aliases [0 ] if aliases else ""
537- pr_body = (
538- f"Changing an entry to `blog/members` for { name } (alias: { first_alias } )."
539- if self .current_file
540- else f"Creating a new entry to `blog/members` for { name } (alias: { first_alias } )."
541- )
542- fork_owner = self .forked_repo .owner .login
543- head_branch = f"{ fork_owner } :main"
544- base_branch = "main"
545-
546- # If editing, retrieve PR by title and push to its branch
547- if self .current_file :
548- # Try to find an open PR with matching title
549- prs = self .original_repo .get_pulls (
550- state = "open" , sort = "created" , base = base_branch
551- )
552- pr_found = None
553- for pr in prs :
554- if self .current_file in pr .title :
555- pr_found = pr
556- break
557- if pr_found :
558- # Push to the PR branch (simulate, as actual branch logic may differ)
559- remote .push ([repo .head .name ], callbacks = callbacks )
560- self .exit (
561- message = f"Archivo { self .current_file } editado, commit y cambios enviados al PR existente."
562- )
563- else :
564- # Otherwise, create a new PR
565- self .original_repo .create_pull (
566- title = pr_title ,
567- body = pr_body ,
568- head = head_branch ,
569- base = base_branch ,
570- )
571- self .exit (
572- message = f"Archivo { self .current_file } guardado, commit y PR listo."
573- )
574- else :
575- # Otherwise, create a new PR
576- self .original_repo .create_pull (
577- title = pr_title ,
578- body = pr_body ,
579- head = head_branch ,
580- base = base_branch ,
581- )
582- self .exit (
583- message = f"Archivo { name_file } .md guardado, commit y PR listo."
584- )
499+ self .exit (message = message )
585500
586501 async def on_event (self , event : Event ) -> None :
587502 # catch listview selection
@@ -609,7 +524,7 @@ def get_repo() -> tuple[str, Repository]:
609524 exit (1 )
610525
611526
612- def fork_repo (token : str , original_repo : Repository ) -> str :
527+ def fork_repo (token : str , original_repo : Repository ) -> tuple [ str , Repository ] :
613528 forked_repo = original_repo .create_fork ()
614529 forked_repo_url = forked_repo .clone_url
615530 repo_path = user_data_dir (appname = "edit-python-pe" , appauthor = "python.pe" )
@@ -622,13 +537,151 @@ def fork_repo(token: str, original_repo: Repository) -> str:
622537 pygit2 .clone_repository (
623538 forked_repo_url , repo_path , callbacks = callbacks
624539 )
625- return repo_path
540+ return repo_path , forked_repo
541+
542+
543+ def compute_file_name (aliases : list [str ], name : str , email : str ) -> str :
544+ # compute name_file
545+ if aliases :
546+ alias_for_name = aliases [0 ].lower ().replace (" " , "_" )
547+ else :
548+ alias_for_name = name .lower ().replace (" " , "_" )
549+
550+ sha_hash = hashlib .sha1 (
551+ (alias_for_name + email + datetime .now ().isoformat ()).encode ("utf-8" )
552+ ).hexdigest ()[:8 ]
553+ return f"{ alias_for_name } -{ sha_hash } .md"
554+
555+
556+ def write_file (file_content : str , file_path : str ) -> None :
557+ os .makedirs (os .path .dirname (file_path ), exist_ok = True )
558+ with open (file_path , "w" , encoding = "utf-8" ) as f :
559+ f .write (file_content )
560+
561+
562+ def commit_and_push (
563+ repo_path : str ,
564+ token : str ,
565+ was_changed : bool ,
566+ name_file : str ,
567+ file_path : str ,
568+ name : str ,
569+ email : str ,
570+ ) -> tuple [
571+ str ,
572+ pygit2 .repository .Repository ,
573+ pygit2 .remotes .Remote ,
574+ pygit2 .callbacks .RemoteCallbacks ,
575+ ]:
576+ repo = pygit2 .repository .Repository (repo_path )
577+ rel_path = os .path .relpath (file_path , repo_path )
578+ rel_path = pathlib .Path (
579+ rel_path
580+ ).as_posix () # Force path to POSIX format so Windows backslashes (\) don't break pygit2
581+ repo .index .add (rel_path )
582+ repo .index .write ()
583+ author_sig = pygit2 .Signature (name or "Unknown" , email or "unknown@email" )
584+ tree_id = repo .index .write_tree ()
585+ parents = [] if repo .head_is_unborn else [repo .head .target ]
586+ commit_msg = (
587+ f"Changed { name_file } " if was_changed else f"Added { name_file } "
588+ )
589+ repo .create_commit (
590+ "HEAD" , author_sig , author_sig , commit_msg , tree_id , parents
591+ )
592+
593+ callbacks = pygit2 .callbacks .RemoteCallbacks (
594+ credentials = pygit2 .UserPass (token , "x-oauth-basic" )
595+ )
596+ remote = repo .remotes ["origin" ]
597+ remote .push ([repo .head .name ], callbacks = callbacks )
598+ return commit_msg , repo , remote , callbacks
599+
600+
601+ def create_pr (
602+ file_content : str ,
603+ current_file : str | None ,
604+ repo_path : str ,
605+ original_repo : Repository ,
606+ forked_repo : Repository ,
607+ token : str ,
608+ aliases : list [str ],
609+ name : str ,
610+ email : str ,
611+ ) -> str :
612+ # Name the file
613+ name_file = (
614+ current_file
615+ if current_file is not None
616+ else compute_file_name (aliases , name , email )
617+ )
618+
619+ # Write file
620+ file_path = os .path .join (repo_path , "blog" , "members" , name_file )
621+ write_file (file_content , file_path )
622+
623+ # commit & push
624+ commit_msg , repo , remote , callbacks = commit_and_push (
625+ repo_path ,
626+ token ,
627+ current_file is not None ,
628+ name_file ,
629+ file_path ,
630+ name ,
631+ email ,
632+ )
633+
634+ # PR logic
635+ pr_title = commit_msg
636+ first_alias = aliases [0 ] if aliases else ""
637+ pr_body = (
638+ f"Changing an entry to `blog/members` for { name } (alias: { first_alias } )."
639+ if current_file
640+ else f"Creating a new entry to `blog/members` for { name } (alias: { first_alias } )."
641+ )
642+ fork_owner = forked_repo .owner .login
643+ head_branch = f"{ fork_owner } :main"
644+ base_branch = "main"
645+
646+ # If editing, retrieve PR by title and push to its branch
647+ if current_file :
648+ # Try to find an open PR with matching title
649+ prs = original_repo .get_pulls (
650+ state = "open" , sort = "created" , base = base_branch
651+ )
652+ pr_found = None
653+ for pr in prs :
654+ if current_file in pr .title :
655+ pr_found = pr
656+ break
657+ if pr_found :
658+ # Push to the PR branch (simulate, as actual branch logic may differ)
659+ remote .push ([repo .head .name ], callbacks = callbacks )
660+ return f"Archivo { name_file } editado, commit y cambios enviados al PR existente."
661+ else :
662+ # Otherwise, create a new PR
663+ original_repo .create_pull (
664+ title = pr_title ,
665+ body = pr_body ,
666+ head = head_branch ,
667+ base = base_branch ,
668+ )
669+ return f"Archivo { name_file } guardado, commit y PR listo."
670+ else :
671+ # Otherwise, create a new PR
672+ original_repo .create_pull (
673+ title = pr_title ,
674+ body = pr_body ,
675+ head = head_branch ,
676+ base = base_branch ,
677+ )
678+ return f"Archivo { name_file } guardado, commit y PR listo."
626679
627680
628681def main () -> None :
629682 token , original_repo = get_repo ()
630- repo_path = fork_repo (token , original_repo )
631- app = MemberApp (repo_path )
683+ repo_path , forked_repo = fork_repo (token , original_repo )
684+ app = MemberApp (original_repo , forked_repo , token , repo_path )
632685 app .run ()
633686
634687
0 commit comments