Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ Thumbs.db
*.csv
results/

# Personal scripts (untracked)
scripts/

53 changes: 41 additions & 12 deletions cli/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
search_prs,
update_complexity_label,
)
from .gitlab import GitLabAPIError
from .csv_handler import CSVBatchWriter
from .io_safety import normalize_path, read_text_file
from .utils import parse_pr_url
from .utils import parse_mr_url

# Get logger
logger = logging.getLogger("complexity-cli")
Expand Down Expand Up @@ -496,7 +497,10 @@ def process_single_pr(

if error:
# Handle 404 errors (PR not found) with a clearer message
if isinstance(error, GitHubAPIError) and error.status_code == 404:
if (
isinstance(error, (GitHubAPIError, GitLabAPIError))
and error.status_code == 404
):
typer.echo(
f"⚠ Skipping {pr_url_result}: PR not found or inaccessible (404)",
err=True,
Expand Down Expand Up @@ -538,7 +542,10 @@ def process_single_pr(

if error:
# Handle 404 errors (PR not found) with a clearer message
if isinstance(error, GitHubAPIError) and error.status_code == 404:
if (
isinstance(error, (GitHubAPIError, GitLabAPIError))
and error.status_code == 404
):
typer.echo(
f"⚠ Skipping {pr_url_result}: PR not found or inaccessible (404)",
err=True,
Expand Down Expand Up @@ -622,9 +629,13 @@ def run_batch_analysis_with_labels(
typer.echo(f" Checked {idx}/{len(pr_urls)} PRs...", err=True)

try:
owner, repo, pr = parse_pr_url(pr_url)
owner_or_project, repo, number, provider, _ = parse_mr_url(pr_url)
if provider == "gitlab":
# GitLab labeling not supported; include for analysis
unlabeled_urls.append(pr_url)
continue
existing_label = has_complexity_label(
owner, repo, pr, github_token, label_prefix, timeout
owner_or_project, repo, number, github_token, label_prefix, timeout
)
if existing_label:
already_labeled += 1
Expand Down Expand Up @@ -692,13 +703,25 @@ def process_single_pr(

label_applied = None

# Apply label if requested
# Apply label if requested (GitHub only)
if label_prs and github_token:
try:
owner, repo, pr = parse_pr_url(pr_url)
label_applied = update_complexity_label(
owner, repo, pr, complexity, github_token, label_prefix, timeout
)
owner_or_project, repo, number, provider, _ = parse_mr_url(pr_url)
if provider == "github":
label_applied = update_complexity_label(
owner_or_project,
repo,
number,
complexity,
github_token,
label_prefix,
timeout,
)
else:
typer.echo(
f" Note: Labeling not supported for GitLab MRs, skipping label for {pr_url}",
err=True,
)
except Exception as label_error:
typer.echo(
f" Warning: Failed to apply label to {pr_url}: {label_error}", err=True
Expand All @@ -723,7 +746,10 @@ def process_single_pr(

if error:
failed_count[0] += 1
if isinstance(error, GitHubAPIError) and error.status_code == 404:
if (
isinstance(error, (GitHubAPIError, GitLabAPIError))
and error.status_code == 404
):
typer.echo(
f"⚠ Skipping {pr_url_result}: PR not found or inaccessible (404)",
err=True,
Expand Down Expand Up @@ -774,7 +800,10 @@ def process_single_pr(
if error:
with completed_lock:
failed_count[0] += 1
if isinstance(error, GitHubAPIError) and error.status_code == 404:
if (
isinstance(error, (GitHubAPIError, GitLabAPIError))
and error.status_code == 404
):
typer.echo(
f"⚠ Skipping {pr_url_result}: PR not found or inaccessible (404)",
err=True,
Expand Down
33 changes: 33 additions & 0 deletions cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,39 @@ def get_github_tokens() -> List[str]:
return []


def get_gitlab_token() -> Optional[str]:
"""Get GitLab token from environment.

Checks GL_TOKEN first, then GITLAB_TOKEN.
"""
return os.getenv("GL_TOKEN") or os.getenv("GITLAB_TOKEN")


def get_gitlab_tokens() -> List[str]:
"""Get multiple GitLab tokens from environment.

Checks GL_TOKENS / GITLAB_TOKENS first (comma-separated), then falls back to single token.

Returns:
List of GitLab tokens (empty list if none found)
"""
tokens_str = os.getenv("GL_TOKENS") or os.getenv("GITLAB_TOKENS")
if tokens_str:
tokens = []
for line in tokens_str.replace("\n", ",").split(","):
token = line.strip()
if token:
tokens.append(token)
if tokens:
return tokens

single_token = get_gitlab_token()
if single_token:
return [single_token]

return []


def get_openai_api_key() -> Optional[str]:
"""Get OpenAI API key from environment."""
return os.getenv("OPENAI_API_KEY")
Expand Down
1 change: 1 addition & 0 deletions cli/config_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AnalysisConfig:

# Credentials (optional - can be provided at runtime)
github_token: Optional[str] = None
gitlab_token: Optional[str] = None
openai_key: Optional[str] = None

# Token rotation (optional)
Expand Down
2 changes: 2 additions & 0 deletions cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@
GITHUB_API_VERSION = "2022-11-28"
GITHUB_API_BASE_URL = "https://api.github.com"
GITHUB_PER_PAGE = 100 # Max items per page for GitHub API
GITLAB_PER_PAGE = 100 # Max items per page for GitLab API
GITLAB_DIFFS_PER_PAGE = 20 # GitLab caps /diffs endpoint at 20 per page (returns 500 above)
46 changes: 46 additions & 0 deletions cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

if TYPE_CHECKING:
from .github import GitHubAPIError
from .gitlab import GitLabAPIError
from .llm import LLMError


Expand Down Expand Up @@ -61,6 +62,51 @@ def handle_github_error(error: "GitHubAPIError") -> None:
else:
typer.echo(f"GitHub API error: {error}", err=True)

@staticmethod
def handle_gitlab_404(project_path: str, mr_iid: int, has_token: bool) -> None:
"""
Handle MR not found errors with helpful hints.

Args:
project_path: GitLab project path
mr_iid: Merge request IID
has_token: Whether a GitLab token was provided
"""
typer.echo("Error: MR not found or not accessible", err=True)
typer.echo(f" Project: {project_path}, MR: !{mr_iid}", err=True)
if not has_token:
typer.echo(
" Hint: If this is a private project, set GL_TOKEN or GITLAB_TOKEN",
err=True,
)
typer.echo(" Example: export GITLAB_TOKEN='your-token'", err=True)
else:
typer.echo(
" Hint: Check that the MR exists and you have access to it",
err=True,
)

@staticmethod
def handle_gitlab_error(error: "GitLabAPIError") -> None:
"""
Handle general GitLab API errors.

Args:
error: The GitLabAPIError that occurred
"""
if error.status_code == 403:
typer.echo("Error: GitLab API access forbidden", err=True)
typer.echo(f" URL: {error.url}", err=True)
typer.echo(" Hint: Check your token has the required permissions", err=True)
elif error.status_code == 401:
typer.echo("Error: GitLab authentication failed", err=True)
typer.echo(" Hint: Check that your token is valid and not expired", err=True)
elif error.status_code == 429:
typer.echo("Error: GitLab API rate limit exceeded", err=True)
typer.echo(" Hint: Wait before retrying or use token rotation", err=True)
else:
typer.echo(f"GitLab API error: {error}", err=True)

@staticmethod
def handle_llm_error(error: "LLMError") -> None:
"""
Expand Down
Loading