Skip to content

Commit 72ede6d

Browse files
committed
deduplication
1 parent c5d3921 commit 72ede6d

4 files changed

Lines changed: 139 additions & 102 deletions

File tree

codeflash/cli_cmds/cli.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
from pathlib import Path
55

66
from codeflash.cli_cmds import logging_config
7-
from codeflash.cli_cmds.cli_common import apologize_and_exit
7+
from codeflash.cli_cmds.cli_common import apologize_and_exit, get_git_repo_or_none, parse_config_file_or_exit
88
from codeflash.cli_cmds.cmd_init import init_codeflash, install_github_actions
99
from codeflash.cli_cmds.console import logger
1010
from codeflash.cli_cmds.extension import install_vscode_extension
1111
from codeflash.code_utils import env_utils
1212
from codeflash.code_utils.code_utils import exit_with_message
13-
from codeflash.code_utils.config_parser import parse_config_file
1413
from codeflash.lsp.helpers import is_LSP_enabled
1514
from codeflash.version import __version__ as version
1615

@@ -163,10 +162,7 @@ def process_and_validate_cmd_args(args: Namespace) -> Namespace:
163162

164163

165164
def process_pyproject_config(args: Namespace) -> Namespace:
166-
try:
167-
pyproject_config, pyproject_file_path = parse_config_file(args.config_file)
168-
except ValueError as e:
169-
exit_with_message(f"Error parsing config file: {e}", error_on_exit=True)
165+
pyproject_config, pyproject_file_path = parse_config_file_or_exit(args.config_file)
170166
supported_keys = [
171167
"module_root",
172168
"tests_root",
@@ -248,21 +244,21 @@ def handle_optimize_all_arg_parsing(args: Namespace) -> Namespace:
248244
no_pr = getattr(args, "no_pr", False)
249245

250246
if not no_pr:
251-
import git
252-
253247
from codeflash.code_utils.git_utils import check_and_push_branch, get_repo_owner_and_name
254248
from codeflash.code_utils.github_utils import require_github_app_or_exit
255249

256250
# Ensure that the user can actually open PRs on the repo.
257-
try:
258-
git_repo = git.Repo(search_parent_directories=True)
259-
except git.exc.InvalidGitRepositoryError:
251+
maybe_git_repo = get_git_repo_or_none()
252+
if maybe_git_repo is None:
260253
mode = "--all" if hasattr(args, "all") else "--file"
261-
logger.exception(
254+
logger.error(
262255
f"I couldn't find a git repository in the current directory. "
263256
f"I need a git repository to run {mode} and open PRs for optimizations. Exiting..."
264257
)
265258
apologize_and_exit()
259+
# After None check and apologize_and_exit(), we know git_repo is not None
260+
git_repo = maybe_git_repo
261+
assert git_repo is not None # For mypy
266262
git_remote = getattr(args, "git_remote", None)
267263
if not check_and_push_branch(git_repo, git_remote=git_remote):
268264
exit_with_message("Branch is not pushed...", error_on_exit=True)

codeflash/cli_cmds/cli_common.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from __future__ import annotations
22

33
import sys
4+
from typing import TYPE_CHECKING, Any, Optional
45

56
from codeflash.cli_cmds.console import console, logger
67

8+
if TYPE_CHECKING:
9+
from pathlib import Path
10+
11+
from git import Repo
12+
713

814
def apologize_and_exit() -> None:
915
console.rule()
@@ -13,3 +19,46 @@ def apologize_and_exit() -> None:
1319
console.rule()
1420
logger.info("👋 Exiting...")
1521
sys.exit(1)
22+
23+
24+
def get_git_repo_or_none(search_path: Optional[Path] = None) -> Optional[Repo]:
25+
"""Get git repository or None if not in a git repo."""
26+
import git
27+
28+
try:
29+
if search_path:
30+
return git.Repo(search_path, search_parent_directories=True)
31+
return git.Repo(search_parent_directories=True)
32+
except git.InvalidGitRepositoryError:
33+
return None
34+
35+
36+
def require_git_repo_or_exit(search_path: Optional[Path] = None, error_message: Optional[str] = None) -> Repo:
37+
"""Get git repository or exit with error."""
38+
repo = get_git_repo_or_none(search_path)
39+
if repo is None:
40+
if error_message:
41+
logger.error(error_message)
42+
else:
43+
logger.error(
44+
"I couldn't find a git repository in the current directory. "
45+
"A git repository is required for this operation."
46+
)
47+
apologize_and_exit()
48+
# After checking for None and calling apologize_and_exit(), we know repo is not None
49+
# but mypy doesn't understand apologize_and_exit() never returns, so we assert
50+
assert repo is not None
51+
return repo
52+
53+
54+
def parse_config_file_or_exit(config_file: Optional[Path] = None, **kwargs: Any) -> tuple[dict[str, Any], Path]:
55+
"""Parse config file or exit with error."""
56+
from codeflash.code_utils.code_utils import exit_with_message
57+
from codeflash.code_utils.config_parser import parse_config_file
58+
59+
try:
60+
return parse_config_file(config_file, **kwargs)
61+
except ValueError as e:
62+
exit_with_message(f"Error parsing config file: {e}", error_on_exit=True)
63+
# exit_with_message never returns when error_on_exit=True, but mypy doesn't know that
64+
raise # pragma: no cover

codeflash/cli_cmds/cmd_init.py

Lines changed: 14 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,15 @@
1010
from pathlib import Path
1111
from typing import TYPE_CHECKING, Any, Optional, Union, cast
1212

13-
import git
1413
import tomlkit
15-
from git import InvalidGitRepositoryError, Repo
1614
from inquirer_textual.common.Choice import Choice
1715
from inquirer_textual.widgets.InquirerConfirm import InquirerConfirm
1816
from inquirer_textual.widgets.InquirerSelect import InquirerSelect
1917
from pydantic.dataclasses import dataclass
2018

21-
from codeflash.api.cfapi import get_user_id, is_github_app_installed_on_repo
19+
from codeflash.api.cfapi import get_user_id
2220
from codeflash.cli_cmds import themed_prompts as prompts
23-
from codeflash.cli_cmds.cli_common import apologize_and_exit
21+
from codeflash.cli_cmds.cli_common import apologize_and_exit, get_git_repo_or_none
2422
from codeflash.cli_cmds.console import console, logger
2523
from codeflash.cli_cmds.extension import install_vscode_extension
2624
from codeflash.cli_cmds.validators import (
@@ -34,8 +32,8 @@
3432
from codeflash.code_utils.compat import LF
3533
from codeflash.code_utils.config_parser import parse_config_file
3634
from codeflash.code_utils.env_utils import check_formatter_installed, get_codeflash_api_key
37-
from codeflash.code_utils.git_utils import get_git_remotes, get_repo_owner_and_name
38-
from codeflash.code_utils.github_utils import get_github_secrets_page_url
35+
from codeflash.code_utils.git_utils import get_git_remotes
36+
from codeflash.code_utils.github_utils import get_github_secrets_page_url, install_github_app
3937
from codeflash.code_utils.oauth_handler import perform_oauth_signin
4038
from codeflash.code_utils.shell_utils import get_shell_rc_path, is_powershell, save_api_key_to_rc
4139
from codeflash.either import is_successful
@@ -236,11 +234,8 @@ def init_codeflash() -> None:
236234

237235
project_name = check_for_toml_or_setup_file()
238236

239-
try:
240-
repo = Repo(curdir, search_parent_directories=True)
241-
git_remotes = get_git_remotes(repo)
242-
except InvalidGitRepositoryError:
243-
git_remotes = []
237+
repo = get_git_repo_or_none(curdir)
238+
git_remotes = get_git_remotes(repo) if repo is not None else []
244239

245240
answers = collect_config(curdir, auth_status, project_name, git_remotes)
246241
git_remote = answers.git_remote
@@ -257,11 +252,8 @@ def init_codeflash() -> None:
257252
if not configure_pyproject_toml(setup_info):
258253
apologize_and_exit()
259254

260-
try:
261-
git.Repo(search_parent_directories=True)
262-
in_git_repo = True
263-
except git.InvalidGitRepositoryError:
264-
in_git_repo = False
255+
maybe_git_repo = get_git_repo_or_none()
256+
in_git_repo = maybe_git_repo is not None
265257

266258
integration_options = [Choice("📦 VSCode Extension", data="vscode")]
267259
if in_git_repo:
@@ -283,7 +275,8 @@ def init_codeflash() -> None:
283275
)
284276
for choice in selected:
285277
if choice.data == "github_app":
286-
install_github_app(git_remote)
278+
if maybe_git_repo is not None:
279+
install_github_app(maybe_git_repo, git_remote)
287280
elif choice.data == "github_actions":
288281
install_github_actions(override_formatter_check=True)
289282
elif choice.data == "vscode":
@@ -541,9 +534,8 @@ def install_github_actions(override_formatter_check: bool = False) -> None: # n
541534
config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check)
542535
ph("cli-github-actions-install-started")
543536

544-
try:
545-
repo = Repo(config["module_root"], search_parent_directories=True)
546-
except git.InvalidGitRepositoryError:
537+
repo = get_git_repo_or_none(Path(config["module_root"]))
538+
if repo is None:
547539
console.print(
548540
"Skipping GitHub action installation for continuous optimization because you're not in a git repository."
549541
)
@@ -846,74 +838,8 @@ def configure_pyproject_toml(
846838
return True
847839

848840

849-
def prompt_github_app_install(owner: str, repo: str) -> None:
850-
"""Prompt user to install GitHub app and wait for confirmation."""
851-
app_url = "https://github.com/apps/codeflash-ai/installations/select_target"
852-
853-
open_page = prompts.select_or_exit(
854-
f"Open GitHub App installation page? ({app_url})",
855-
choices=["Yes", "No"],
856-
default="Yes",
857-
header=(
858-
f"🐙 GitHub App Installation\n\n"
859-
f"You'll need to install the Codeflash GitHub app for {owner}/{repo}.\n\n"
860-
"I'll open the installation page where you can select your repository."
861-
),
862-
)
863-
if open_page == "Yes":
864-
webbrowser.open(app_url)
865-
866-
prompts.confirm("Continue once you've completed the installation?", default=True)
867-
868-
869-
def install_github_app(git_remote: str) -> None:
870-
try:
871-
git_repo = git.Repo(search_parent_directories=True)
872-
except git.InvalidGitRepositoryError:
873-
console.print("Skipping GitHub app installation because you're not in a git repository.")
874-
return
875-
876-
if git_remote not in get_git_remotes(git_repo):
877-
console.print(f"Skipping GitHub app installation, remote ({git_remote}) does not exist in this repository.")
878-
return
879-
880-
owner, repo = get_repo_owner_and_name(git_repo, git_remote)
881-
882-
if is_github_app_installed_on_repo(owner, repo, suppress_errors=True):
883-
console.print(
884-
f"🐙 Looks like you've already installed the Codeflash GitHub app on this repository ({owner}/{repo})! Continuing…"
885-
)
886-
return
887-
888-
# Not installed - prompt for installation
889-
try:
890-
prompt_github_app_install(owner, repo)
891-
892-
# Verify installation with retries
893-
max_retries = 2
894-
for attempt in range(max_retries + 1):
895-
if is_github_app_installed_on_repo(owner, repo, suppress_errors=True):
896-
console.print(f"✅ GitHub App installed successfully for {owner}/{repo}!")
897-
break
898-
899-
if attempt == max_retries:
900-
console.print(
901-
f"❌ GitHub App not detected on {owner}/{repo}.\n"
902-
f"You won't be able to create PRs until you install it.\n"
903-
f"Use the '--no-pr' flag for local-only optimizations."
904-
)
905-
break
906-
907-
console.print(
908-
f"❌ GitHub App not detected on {owner}/{repo}.\nPress Enter to check again after installation..."
909-
)
910-
console.input()
911-
except (KeyboardInterrupt, EOFError):
912-
console.print() # Clean line for next prompt
913-
914-
915841
def save_api_key_and_set_env(api_key: str) -> None:
916-
"""Save API key to shell RC file and set environment variable."""
842+
"""Save API key to shell RC and set env var."""
917843
shell_rc_path = get_shell_rc_path()
918844
if not shell_rc_path.exists() and os.name == "nt":
919845
shell_rc_path.parent.mkdir(parents=True, exist_ok=True)
@@ -931,7 +857,7 @@ def save_api_key_and_set_env(api_key: str) -> None:
931857

932858

933859
def enter_api_key_and_save_to_rc() -> None:
934-
"""Prompt for API key with validation and save to shell RC file."""
860+
"""Prompt for API key and save to shell RC."""
935861
browser_launched = False
936862
api_key = ""
937863

codeflash/code_utils/github_utils.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from __future__ import annotations
22

3+
import webbrowser
34
from typing import TYPE_CHECKING, Optional
45

56
from codeflash.api.cfapi import is_github_app_installed_on_repo
67
from codeflash.cli_cmds.cli_common import apologize_and_exit
7-
from codeflash.cli_cmds.console import paneled_text
8+
from codeflash.cli_cmds.console import console, paneled_text
89
from codeflash.code_utils.compat import LF
9-
from codeflash.code_utils.git_utils import get_repo_owner_and_name
10+
from codeflash.code_utils.git_utils import get_git_remotes, get_repo_owner_and_name
1011

1112
if TYPE_CHECKING:
13+
import git
1214
from git import Repo
1315

1416

@@ -38,3 +40,67 @@ def require_github_app_or_exit(owner: str, repo: str) -> None:
3840

3941
def github_pr_url(owner: str, repo: str, pr_number: str) -> str:
4042
return f"https://github.com/{owner}/{repo}/pull/{pr_number}"
43+
44+
45+
def prompt_github_app_install(owner: str, repo: str) -> None:
46+
"""Prompt user to install GitHub app."""
47+
# Avoid circular import
48+
from codeflash.cli_cmds import themed_prompts as prompts
49+
50+
app_url = "https://github.com/apps/codeflash-ai/installations/select_target"
51+
52+
open_page = prompts.select_or_exit(
53+
f"Open GitHub App installation page? ({app_url})",
54+
choices=["Yes", "No"],
55+
default="Yes",
56+
header=(
57+
f"🐙 GitHub App Installation\n\n"
58+
f"You'll need to install the Codeflash GitHub app for {owner}/{repo}.\n\n"
59+
"I'll open the installation page where you can select your repository."
60+
),
61+
)
62+
if open_page == "Yes":
63+
webbrowser.open(app_url)
64+
65+
prompts.confirm("Continue once you've completed the installation?", default=True)
66+
67+
68+
def install_github_app(git_repo: git.Repo, git_remote: str = "origin") -> None:
69+
"""Install GitHub app with user prompts and verification."""
70+
if git_remote not in get_git_remotes(git_repo):
71+
console.print(f"Skipping GitHub app installation, remote ({git_remote}) does not exist in this repository.")
72+
return
73+
74+
owner, repo = get_repo_owner_and_name(git_repo, git_remote)
75+
76+
if is_github_app_installed_on_repo(owner, repo, suppress_errors=True):
77+
console.print(
78+
f"🐙 Looks like you've already installed the Codeflash GitHub app on this repository ({owner}/{repo})! Continuing…"
79+
)
80+
return
81+
82+
# Not installed - prompt for installation
83+
try:
84+
prompt_github_app_install(owner, repo)
85+
86+
# Verify installation with retries
87+
max_retries = 2
88+
for attempt in range(max_retries + 1):
89+
if is_github_app_installed_on_repo(owner, repo, suppress_errors=True):
90+
console.print(f"✅ GitHub App installed successfully for {owner}/{repo}!")
91+
break
92+
93+
if attempt == max_retries:
94+
console.print(
95+
f"❌ GitHub App not detected on {owner}/{repo}.\n"
96+
f"You won't be able to create PRs until you install it.\n"
97+
f"Use the '--no-pr' flag for local-only optimizations."
98+
)
99+
break
100+
101+
console.print(
102+
f"❌ GitHub App not detected on {owner}/{repo}.\nPress Enter to check again after installation..."
103+
)
104+
console.input()
105+
except (KeyboardInterrupt, EOFError):
106+
console.print() # Clean line for next prompt

0 commit comments

Comments
 (0)