From 7c554f2c40c37f46f9aadaaffaf9a48cb21aa9a7 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Sat, 1 Nov 2025 18:26:10 +0800 Subject: [PATCH 1/9] Implement hands on hp-push-tags --- exercise_utils/git.py | 15 +++++++++++++++ hands_on/push_tags.py | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 hands_on/push_tags.py diff --git a/exercise_utils/git.py b/exercise_utils/git.py index 5df9919..8e2bb25 100644 --- a/exercise_utils/git.py +++ b/exercise_utils/git.py @@ -10,6 +10,21 @@ def tag(tag_name: str, verbose: bool) -> None: run_command(["git", "tag", tag_name], verbose) +def annotated_tag(tag_name: str, verbose: bool) -> None: + """Adds an annotated tag to the latest commit with the given tag_name.""" + run_command(["git", "tag", "-a", tag_name], verbose) + + +def tag_with_options(tag_name: str, options: List[str], verbose: bool) -> None: + """Tags with the given tag_name with specified options.""" + run_command(["git", "tag", tag_name, *options], verbose) + + +def annotated_tag_with_options(tag_name: str, options: List[str], verbose: bool) -> None: + """Adds an annotated tag with the given tag_name with specified options.""" + run_command(["git", "tag", "-a", tag_name, *options], verbose) + + def add(files: List[str], verbose: bool) -> None: """Adds a given list of file paths.""" run_command(["git", "add", *files], verbose) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py new file mode 100644 index 0000000..19a3ac3 --- /dev/null +++ b/hands_on/push_tags.py @@ -0,0 +1,26 @@ +import os +from exercise_utils.cli import run_command +from exercise_utils.git import tag_with_options, annotated_tag_with_options + +__requires_git__ = True +__requires_github__ = True + +REPO_NAME = "samplerepo-preferences" + + +def download(verbose: bool): + + run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", "--clone"], + verbose) + + if os.path.isdir(REPO_NAME): + os.chdir(REPO_NAME) + else: + # If user already has an repo with the same name, the gh command will + # atomatically create the fork with the name appended with "-1" + os.chdir(f"{REPO_NAME}-1") + + tag_with_options("v1.0", ["HEAD~1"], verbose) + annotated_tag_with_options("v0.9", ["HEAD~2", "-m", "First beta release"], verbose) + + pass From 598a3b8f13f4ed24c1d43bbdd12ad9ab9db26d80 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Sat, 1 Nov 2025 18:56:30 +0800 Subject: [PATCH 2/9] Fix the cloned repo directory name to REPO_NAME --- hands_on/push_tags.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 19a3ac3..886c6b0 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -10,15 +10,10 @@ def download(verbose: bool): - run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", "--clone"], + run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", REPO_NAME, "--clone"], verbose) - if os.path.isdir(REPO_NAME): - os.chdir(REPO_NAME) - else: - # If user already has an repo with the same name, the gh command will - # atomatically create the fork with the name appended with "-1" - os.chdir(f"{REPO_NAME}-1") + os.chdir(REPO_NAME) tag_with_options("v1.0", ["HEAD~1"], verbose) annotated_tag_with_options("v0.9", ["HEAD~2", "-m", "First beta release"], verbose) From 5dc73ff3b1bb4e9c142a4db6076c5afd9e60dd77 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Mon, 3 Nov 2025 00:23:52 +0800 Subject: [PATCH 3/9] Notify user when fork name is changed --- hands_on/push_tags.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 886c6b0..c4b6a22 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -1,4 +1,5 @@ import os +import subprocess from exercise_utils.cli import run_command from exercise_utils.git import tag_with_options, annotated_tag_with_options @@ -10,8 +11,24 @@ def download(verbose: bool): - run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", REPO_NAME, "--clone"], - verbose) + # Cannot use run_command function because returned object should not be None. + username = subprocess.run( + ["gh", "api", "user", "-q", ".login"], + capture_output=True, + text=True, + ).stdout.strip() + + # Cannot use run_command function because return code is needed. + check_repo = subprocess.run( + ["gh", "repo", "view", f"{username}/{REPO_NAME}"], + capture_output=True, + text=True, + ) + + if check_repo.returncode == 0: + print(f"{username}/{REPO_NAME} already exists, the fork repo will be named as {username}/{REPO_NAME}-1") + + run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", REPO_NAME, "--clone"], verbose) os.chdir(REPO_NAME) From d851e1d0dc0c5890007bc34d84ea4a2ba98a07b7 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Mon, 3 Nov 2025 01:41:50 +0800 Subject: [PATCH 4/9] Abort script when a fork already exists --- hands_on/push_tags.py | 44 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index c4b6a22..533b24b 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -8,25 +8,55 @@ REPO_NAME = "samplerepo-preferences" - -def download(verbose: bool): - +def get_username() -> str: # Cannot use run_command function because returned object should not be None. - username = subprocess.run( + return subprocess.run( ["gh", "api", "user", "-q", ".login"], capture_output=True, text=True, ).stdout.strip() + +def check_same_repo_name(username: str, repo_name: str) -> None: # Cannot use run_command function because return code is needed. check_repo = subprocess.run( - ["gh", "repo", "view", f"{username}/{REPO_NAME}"], + ["gh", "repo", "view", f"{username}/{repo_name}"], capture_output=True, text=True, ) - if check_repo.returncode == 0: - print(f"{username}/{REPO_NAME} already exists, the fork repo will be named as {username}/{REPO_NAME}-1") + print(f"Warning: {username}/{REPO_NAME} already exists, the fork repo will be " + f"named as {username}/{REPO_NAME}-1") + +def check_existing_fork(username: str, fork_owner_name: str, repo_name: str) -> None: + try: + result = subprocess.run( + ["gh", + "api", + f"repos/{fork_owner_name}/{repo_name}/forks", + "-q", + f'''.[] | .owner.login | select(. =="{username}")''', + ], + capture_output=True, + text=True, + check=True, + ) + if result.stdout == username: + print(f"ERROR: A fork of {fork_owner_name}/{repo_name} already exists! " + "Please delete the fork and run this download operation again.\n" + "!Aborting...") + exit(1) + except subprocess.CalledProcessError as e: + print(e.stderr) + exit(1) + + + + +def download(verbose: bool): + username = get_username() + check_existing_fork(username, "git-mastery", REPO_NAME) + check_same_repo_name(username, REPO_NAME) run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", REPO_NAME, "--clone"], verbose) From 0b48ebf9a80e55d12c69a5e5b5091e06a57c0dce Mon Sep 17 00:00:00 2001 From: dingZvel Date: Thu, 20 Nov 2025 19:43:18 +0800 Subject: [PATCH 5/9] Refactor using core util functions for gh --- hands_on/push_tags.py | 83 +++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 533b24b..d9fbfe6 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -1,68 +1,59 @@ import os -import subprocess -from exercise_utils.cli import run_command +from exercise_utils.cli import run from exercise_utils.git import tag_with_options, annotated_tag_with_options +from exercise_utils.github_cli import clone_repo_with_gh, fork_repo, get_github_username, has_repo __requires_git__ = True __requires_github__ = True -REPO_NAME = "samplerepo-preferences" - -def get_username() -> str: - # Cannot use run_command function because returned object should not be None. - return subprocess.run( - ["gh", "api", "user", "-q", ".login"], - capture_output=True, - text=True, - ).stdout.strip() - - -def check_same_repo_name(username: str, repo_name: str) -> None: - # Cannot use run_command function because return code is needed. - check_repo = subprocess.run( - ["gh", "repo", "view", f"{username}/{repo_name}"], - capture_output=True, - text=True, +def check_existing_fork(username: str, fork_owner_name: str, repo_name: str, verbose: bool) -> None: + result = run( + ["gh", + "api", + f"repos/{fork_owner_name}/{repo_name}/forks", + "-q", + f'''.[] | .owner.login | select(. =="{username}")''', + ], + verbose ) - if check_repo.returncode == 0: - print(f"Warning: {username}/{REPO_NAME} already exists, the fork repo will be " - f"named as {username}/{REPO_NAME}-1") - -def check_existing_fork(username: str, fork_owner_name: str, repo_name: str) -> None: - try: - result = subprocess.run( - ["gh", - "api", - f"repos/{fork_owner_name}/{repo_name}/forks", - "-q", - f'''.[] | .owner.login | select(. =="{username}")''', - ], - capture_output=True, - text=True, - check=True, - ) + if result.is_success(): if result.stdout == username: print(f"ERROR: A fork of {fork_owner_name}/{repo_name} already exists! " "Please delete the fork and run this download operation again.\n" "!Aborting...") exit(1) - except subprocess.CalledProcessError as e: - print(e.stderr) - exit(1) +def check_same_repo_name(username: str, repo_name: str, verbose: bool) -> str: + check_repo = run( + ["gh", "repo", "view", f"{username}/{repo_name}"], + verbose + ) + if check_repo.is_success(): + print(f"Warning: {username}/{repo_name} already exists, the fork repo will be " + f"named as {username}/{repo_name}-1") + return repo_name + "-1" + return repo_name + + # if has_repo(repo_name, False, verbose): + # print(f"Warning: {username}/{repo_name} already exists, the fork repo will be " + # f"named as {username}/{repo_name}-1") + # return repo_name + "-1" + # return repo_name +def download(verbose: bool): + REPO_NAME = "samplerepo-preferences" + FORK_NAME = "gitmastery-samplerepo-preferences" + username = get_github_username(verbose) + check_existing_fork(username, "git-mastery", REPO_NAME, verbose) + FORK_NAME = check_same_repo_name(username, FORK_NAME, verbose) -def download(verbose: bool): - username = get_username() - check_existing_fork(username, "git-mastery", REPO_NAME) - check_same_repo_name(username, REPO_NAME) + fork_repo(f"git-mastery/{REPO_NAME}", FORK_NAME, verbose) + clone_repo_with_gh(FORK_NAME, verbose) - run_command(["gh", "repo", "fork", f"git-mastery/{REPO_NAME}", REPO_NAME, "--clone"], verbose) - os.chdir(REPO_NAME) + os.chdir(FORK_NAME) tag_with_options("v1.0", ["HEAD~1"], verbose) annotated_tag_with_options("v0.9", ["HEAD~2", "-m", "First beta release"], verbose) - pass From f003982fba60114e9c339de79601ab1da41df8e2 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Thu, 20 Nov 2025 19:53:43 +0800 Subject: [PATCH 6/9] Refactor using has_repo util function --- hands_on/push_tags.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index d9fbfe6..35f177c 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -24,22 +24,12 @@ def check_existing_fork(username: str, fork_owner_name: str, repo_name: str, ver exit(1) def check_same_repo_name(username: str, repo_name: str, verbose: bool) -> str: - check_repo = run( - ["gh", "repo", "view", f"{username}/{repo_name}"], - verbose - ) - if check_repo.is_success(): + if has_repo(repo_name, False, verbose): print(f"Warning: {username}/{repo_name} already exists, the fork repo will be " - f"named as {username}/{repo_name}-1") + f"named as {username}/{repo_name}-1") return repo_name + "-1" return repo_name - # if has_repo(repo_name, False, verbose): - # print(f"Warning: {username}/{repo_name} already exists, the fork repo will be " - # f"named as {username}/{repo_name}-1") - # return repo_name + "-1" - # return repo_name - def download(verbose: bool): REPO_NAME = "samplerepo-preferences" FORK_NAME = "gitmastery-samplerepo-preferences" From e1ac3108a7e52185ca14ae84c33aaa7154724cc4 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Thu, 20 Nov 2025 20:07:49 +0800 Subject: [PATCH 7/9] Fix the name of directory to "gitmastery-samplerepo-preferences" --- hands_on/push_tags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 35f177c..6e2ccda 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -36,10 +36,10 @@ def download(verbose: bool): username = get_github_username(verbose) check_existing_fork(username, "git-mastery", REPO_NAME, verbose) - FORK_NAME = check_same_repo_name(username, FORK_NAME, verbose) + NEW_FORK_NAME = check_same_repo_name(username, FORK_NAME, verbose) - fork_repo(f"git-mastery/{REPO_NAME}", FORK_NAME, verbose) - clone_repo_with_gh(FORK_NAME, verbose) + fork_repo(f"git-mastery/{REPO_NAME}", NEW_FORK_NAME, verbose) + clone_repo_with_gh(NEW_FORK_NAME, verbose, FORK_NAME) os.chdir(FORK_NAME) From bac480733310bea21505c061604bfcb87e3d205e Mon Sep 17 00:00:00 2001 From: dingZvel Date: Fri, 5 Dec 2025 11:29:37 +0800 Subject: [PATCH 8/9] Skip and clone when an existing fork is found --- exercise_utils/github_cli.py | 33 +++++++++++++++++++++++++++++++++ hands_on/push_tags.py | 32 ++++++++------------------------ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/exercise_utils/github_cli.py b/exercise_utils/github_cli.py index 235602d..2b3ad2c 100644 --- a/exercise_utils/github_cli.py +++ b/exercise_utils/github_cli.py @@ -64,3 +64,36 @@ def has_repo(repo_name: str, is_fork: bool, verbose: bool) -> bool: ) return result.is_success() and (not is_fork or result.stdout == "true") + +def has_fork(repository_name: str, owner_name: str, username: str, verbose: bool) -> bool: + """Returns if the current user has a fork of the given repository by owner""" + result = run( + [ + "gh", + "api", + f"repos/{owner_name}/{repository_name}/forks", + "-q", + f'''.[] | .owner.login | select(. =="{username}")''', + ], + verbose + ) + + return result.is_success() and result.stdout == username + +def get_fork_name(repository_name: str, owner_name: str, username: str, verbose: bool) -> str: + """Returns the name of the current user's fork repo""" + result = run( + [ + "gh", + "api", + f"repos/{owner_name}/{repository_name}/forks", + "-q", + f'''.[] | select(.owner.login =="{username}") | .name''', + ], + verbose + ) + + if result.is_success(): + forkname = result.stdout.splitlines()[0] + return forkname + return "" diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 6e2ccda..763253d 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -1,28 +1,11 @@ import os from exercise_utils.cli import run from exercise_utils.git import tag_with_options, annotated_tag_with_options -from exercise_utils.github_cli import clone_repo_with_gh, fork_repo, get_github_username, has_repo +from exercise_utils.github_cli import clone_repo_with_gh, fork_repo, get_fork_name, get_github_username, has_fork, has_repo __requires_git__ = True __requires_github__ = True -def check_existing_fork(username: str, fork_owner_name: str, repo_name: str, verbose: bool) -> None: - result = run( - ["gh", - "api", - f"repos/{fork_owner_name}/{repo_name}/forks", - "-q", - f'''.[] | .owner.login | select(. =="{username}")''', - ], - verbose - ) - if result.is_success(): - if result.stdout == username: - print(f"ERROR: A fork of {fork_owner_name}/{repo_name} already exists! " - "Please delete the fork and run this download operation again.\n" - "!Aborting...") - exit(1) - def check_same_repo_name(username: str, repo_name: str, verbose: bool) -> str: if has_repo(repo_name, False, verbose): print(f"Warning: {username}/{repo_name} already exists, the fork repo will be " @@ -35,12 +18,13 @@ def download(verbose: bool): FORK_NAME = "gitmastery-samplerepo-preferences" username = get_github_username(verbose) - check_existing_fork(username, "git-mastery", REPO_NAME, verbose) - NEW_FORK_NAME = check_same_repo_name(username, FORK_NAME, verbose) - - fork_repo(f"git-mastery/{REPO_NAME}", NEW_FORK_NAME, verbose) - clone_repo_with_gh(NEW_FORK_NAME, verbose, FORK_NAME) - + if has_fork(REPO_NAME, "git-mastery", username, verbose): + existing_name = get_fork_name(REPO_NAME, "git-mastery", username, verbose) + clone_repo_with_gh(existing_name, verbose, FORK_NAME) + else: + NEW_FORK_NAME = check_same_repo_name(username, FORK_NAME, verbose) + fork_repo(f"git-mastery/{REPO_NAME}", NEW_FORK_NAME, verbose) + clone_repo_with_gh(NEW_FORK_NAME, verbose, FORK_NAME) os.chdir(FORK_NAME) From 246e7c3a81f9a3a0e0fdd8c73726c3075e957535 Mon Sep 17 00:00:00 2001 From: dingZvel Date: Mon, 8 Dec 2025 15:56:56 +0800 Subject: [PATCH 9/9] Fix based on VikramGoyal23's review --- exercise_utils/git.py | 5 ----- exercise_utils/github_cli.py | 4 +++- hands_on/push_tags.py | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/exercise_utils/git.py b/exercise_utils/git.py index 9cd4cfb..2f12ed2 100644 --- a/exercise_utils/git.py +++ b/exercise_utils/git.py @@ -10,11 +10,6 @@ def tag(tag_name: str, verbose: bool) -> None: run_command(["git", "tag", tag_name], verbose) -def annotated_tag(tag_name: str, verbose: bool) -> None: - """Adds an annotated tag to the latest commit with the given tag_name.""" - run_command(["git", "tag", "-a", tag_name], verbose) - - def tag_with_options(tag_name: str, options: List[str], verbose: bool) -> None: """Tags with the given tag_name with specified options.""" run_command(["git", "tag", tag_name, *options], verbose) diff --git a/exercise_utils/github_cli.py b/exercise_utils/github_cli.py index 2b3ad2c..8c354a3 100644 --- a/exercise_utils/github_cli.py +++ b/exercise_utils/github_cli.py @@ -71,6 +71,7 @@ def has_fork(repository_name: str, owner_name: str, username: str, verbose: bool [ "gh", "api", + "--paginate", f"repos/{owner_name}/{repository_name}/forks", "-q", f'''.[] | .owner.login | select(. =="{username}")''', @@ -78,7 +79,7 @@ def has_fork(repository_name: str, owner_name: str, username: str, verbose: bool verbose ) - return result.is_success() and result.stdout == username + return result.is_success() and result.stdout.strip() == username def get_fork_name(repository_name: str, owner_name: str, username: str, verbose: bool) -> str: """Returns the name of the current user's fork repo""" @@ -86,6 +87,7 @@ def get_fork_name(repository_name: str, owner_name: str, username: str, verbose: [ "gh", "api", + "--paginate", f"repos/{owner_name}/{repository_name}/forks", "-q", f'''.[] | select(.owner.login =="{username}") | .name''', diff --git a/hands_on/push_tags.py b/hands_on/push_tags.py index 763253d..cf69ed8 100644 --- a/hands_on/push_tags.py +++ b/hands_on/push_tags.py @@ -1,5 +1,4 @@ import os -from exercise_utils.cli import run from exercise_utils.git import tag_with_options, annotated_tag_with_options from exercise_utils.github_cli import clone_repo_with_gh, fork_repo, get_fork_name, get_github_username, has_fork, has_repo