From ede489c2198cc5f3430ad2f42da6e65a75ea1887 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 16 Jun 2026 14:36:19 +0545 Subject: [PATCH] fix: resolve 8 bugs affecting real users - Fix KeyError 'root' in FileGrouper when merging groups with no root-level files - Fix changelog commit parsing breaking on multiline bodies (use %x00 separator) - Add empty choices/content checks in OpenAI, Anthropic, OpenRouter providers - Fix config edit crash when config is empty - Fix validate command using wrong color for 'no staged files' - Add type check for corrupted YAML config (non-dict returns empty dict) --- devgen/cli/commit.py | 2 +- devgen/cli/config.py | 6 ++++++ devgen/modules/changelog_generator.py | 10 +++++++--- devgen/modules/diff_builder.py | 5 +---- devgen/providers/anthropic.py | 2 ++ devgen/providers/openai.py | 2 ++ devgen/providers/openrouter.py | 4 +++- devgen/utils.py | 8 +++++++- 8 files changed, 29 insertions(+), 10 deletions(-) diff --git a/devgen/cli/commit.py b/devgen/cli/commit.py index d9b455e..babae20 100644 --- a/devgen/cli/commit.py +++ b/devgen/cli/commit.py @@ -120,7 +120,7 @@ def validate() -> None: for f in staged: typer.echo(f"- {f}") else: - typer.secho("[i] No staged files.", fg=typer.colors.RED) + typer.secho("[i] No staged files.", fg=typer.colors.YELLOW) @app.command("undo") diff --git a/devgen/cli/config.py b/devgen/cli/config.py index 762c0a0..e4a574d 100644 --- a/devgen/cli/config.py +++ b/devgen/cli/config.py @@ -48,6 +48,12 @@ def edit_config( if not key: choices = list(config.keys()) + if not choices: + typer.secho( + "No configuration keys found. Run `devgen setup config` first.", + fg=typer.colors.YELLOW, + ) + raise typer.Exit() key = questionary.select( "Select key to update:", choices=choices, style=style ).ask() diff --git a/devgen/modules/changelog_generator.py b/devgen/modules/changelog_generator.py index 4339f86..16a7f8c 100644 --- a/devgen/modules/changelog_generator.py +++ b/devgen/modules/changelog_generator.py @@ -32,17 +32,21 @@ class ChangelogGenerator: else: cmd = self._resolve_range(to_ref) try: - return run_git_command(cmd).split("\n") + output = run_git_command(cmd) + # Use %x00 as separator to avoid splitting on newlines in commit bodies + # Then filter out empty strings + return [c for c in output.split("\0") if c.strip()] except subprocess.CalledProcessError as e: self.logger.error(f"Git command failed: {e}") raise RuntimeError(f"Git command failed: {e}") from e def _log_cmd(self, range_spec: str) -> List[str]: - # Format: hash|author|date|subject|body + # Format: hash|author|date|subject|body (separated by %x00 to avoid + # splitting on newlines within commit bodies) return [ "git", "log", - "--format=%H|%an|%ad|%s|%b", + "--format=%H|%an|%ad|%s|%b%x00", "--date=short", range_spec, ] diff --git a/devgen/modules/diff_builder.py b/devgen/modules/diff_builder.py index 7c80a14..e72856a 100644 --- a/devgen/modules/diff_builder.py +++ b/devgen/modules/diff_builder.py @@ -129,10 +129,7 @@ class FileGrouper: parent_path = str(Path(deepest).parent) new_key = "root" if parent_path == "." else parent_path files = groups.pop(deepest) - if new_key in groups: - groups[new_key].extend(files) - else: - groups["root"].extend(files) + groups.setdefault(new_key, []).extend(files) return groups diff --git a/devgen/providers/anthropic.py b/devgen/providers/anthropic.py index 73d5336..f0780bd 100644 --- a/devgen/providers/anthropic.py +++ b/devgen/providers/anthropic.py @@ -16,4 +16,6 @@ class AnthropicProvider(BaseProvider): max_tokens=1024, messages=[{"role": "user", "content": prompt}], ) + if not message.content: + raise RuntimeError(f"Anthropic returned no content for model {model!r}.") return message.content[0].text or "" diff --git a/devgen/providers/openai.py b/devgen/providers/openai.py index 199837f..5a2a9df 100644 --- a/devgen/providers/openai.py +++ b/devgen/providers/openai.py @@ -17,4 +17,6 @@ class OpenaiProvider(BaseProvider): messages=[{"role": "user", "content": prompt}], **kwargs, ) + if not response.choices: + raise RuntimeError(f"OpenAI returned no choices for model {model!r}.") return response.choices[0].message.content or "" diff --git a/devgen/providers/openrouter.py b/devgen/providers/openrouter.py index 6c98296..23f4f02 100644 --- a/devgen/providers/openrouter.py +++ b/devgen/providers/openrouter.py @@ -22,4 +22,6 @@ class OpenrouterProvider(BaseProvider): }, **kwargs, ) - return response.choices[0].message.content + if not response.choices: + raise RuntimeError(f"OpenRouter returned no choices for model {model!r}.") + return response.choices[0].message.content or "" diff --git a/devgen/utils.py b/devgen/utils.py index fec659c..9739dc1 100644 --- a/devgen/utils.py +++ b/devgen/utils.py @@ -246,7 +246,13 @@ def load_config() -> Dict[str, Any]: try: with config_path.open("r", encoding="utf-8") as f: - return yaml.safe_load(f) or {} + data = yaml.safe_load(f) + if not isinstance(data, dict): + print( + f"Warning: Config at {config_path} is not a dict, using defaults." + ) + return {} + return data except Exception as e: print(f"Warning: Failed to load config from {config_path}: {e}") return {}