diff --git a/devgen/modules/changelog_generator.py b/devgen/modules/changelog_generator.py index 6c3634a..f618f81 100644 --- a/devgen/modules/changelog_generator.py +++ b/devgen/modules/changelog_generator.py @@ -87,16 +87,16 @@ class ChangelogGenerator: groups["Bug Fixes"].append(entry) elif c_type in ["docs"]: groups["Documentation"].append(entry) - elif c_type in [ - "style", - "refactor", - "perf", - "test", - "build", - "ci", - "chore", - ]: - groups["Other Changes"].append(entry) + elif c_type == "perf": + groups["Performance"].append(entry) + elif c_type == "refactor": + groups["Refactor"].append(entry) + elif c_type == "test": + groups["Tests"].append(entry) + elif c_type == "style": + groups["Style"].append(entry) + elif c_type in ["build", "ci", "chore"]: + groups["Chore"].append(entry) else: groups["Other Changes"].append(entry) else: @@ -122,12 +122,17 @@ class ChangelogGenerator: date_str = datetime.now().strftime("%Y-%m-%d") md = [f"## {version} ({date_str})\n"] - # Order: Breaking, Features, Fixes, Docs, Others + # Order: Breaking, Features, Fixes, Docs, Refactor, Perf, Tests, Style, Chore, Others order = [ "BREAKING CHANGES", "Features", "Bug Fixes", + "Performance", "Documentation", + "Refactor", + "Tests", + "Style", + "Chore", "Other Changes", ] @@ -135,8 +140,13 @@ class ChangelogGenerator: "BREAKING CHANGES": "๐Ÿ’ฅ BREAKING CHANGES", "Features": "โœจ Features", "Bug Fixes": "๐Ÿ› Bug Fixes", + "Performance": "โšก Performance", "Documentation": "๐Ÿ“š Documentation", - "Other Changes": "๐Ÿ”จ Other Changes", + "Refactor": "โ™ป๏ธ Refactor", + "Tests": "โœ… Tests", + "Style": "๐Ÿ’„ Style", + "Chore": "๐Ÿ”ง Chore", + "Other Changes": "๐Ÿงน Other Changes", } for section in order: diff --git a/devgen/modules/commit_generator.py b/devgen/modules/commit_generator.py index ce6e634..e989759 100644 --- a/devgen/modules/commit_generator.py +++ b/devgen/modules/commit_generator.py @@ -244,6 +244,26 @@ class CommitEngine: self.logger.info("Pushing to remote...") with self.console.status("[bold green]Pushing to remote...[/bold green]"): try: + # Fetch first so a non-fast-forward push fails fast with a + # clear message instead of getting rejected by the remote. + try: + upstream = run_git_command( + [ + "git", + "rev-parse", + "--abbrev-ref", + "--symbolic-full-name", + "@{u}", + ], + check=False, + ) + if upstream: + run_git_command( + ["git", "fetch", "origin", upstream], check=False + ) + except (subprocess.CalledProcessError, CommitEngineError): + pass + run_git_command(["git", "push"]) except subprocess.CalledProcessError as e: msg = ( @@ -348,22 +368,38 @@ class CommitEngine: ) return sanitize_ai_commit_message(raw) - def is_ahead_of_remote(self) -> bool: - """Checks if local branch has unpushed commits.""" + def is_ahead_of_remote(self, fetch: bool = False) -> bool: + """Checks if local branch has unpushed commits. + + Args: + fetch: When True, refresh remote refs first (call only when you + intend to push). Avoids an unconditional network call on + every run and a TOCTOU window between fetch and rev-list. + """ + try: + upstream = run_git_command( + ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], + check=False, + ) + except (subprocess.CalledProcessError, CommitEngineError): + upstream = "" + + if not upstream: + return False + + if fetch: + try: + run_git_command(["git", "fetch", "origin", upstream], check=False) + except (subprocess.CalledProcessError, CommitEngineError): + pass + try: - run_git_command(["git", "fetch", "origin"]) count = run_git_command( ["git", "rev-list", "--count", "@{u}..HEAD"], check=False ) - if count and int(count) > 0: - return True - except (subprocess.CalledProcessError, CommitEngineError): - # Maybe no upstream - try: - return bool(run_git_command(["git", "rev-parse", "HEAD"], check=False)) - except subprocess.CalledProcessError: - return False - return False + return bool(count) and int(count) > 0 + except (subprocess.CalledProcessError, CommitEngineError, ValueError): + return False def load_cache(self) -> Dict[str, str]: """Loads dry-run cache.""" @@ -441,7 +477,7 @@ class CommitEngine: return True except Exception as e: - self.logger.error(f"Failed to process group {group}: {e}") + self.logger.error(f"Failed to process group {group!r}: {e}", exc_info=True) self._reset_group(files) return False @@ -455,7 +491,9 @@ class CommitEngine: def execute(self): """Main execution method.""" files = self.detect_changes() - ahead = self.is_ahead_of_remote() + # Only consult the remote state if the user actually wants to push. + # This avoids a network round-trip on every run and a TOCTOU window. + ahead = self.is_ahead_of_remote(fetch=self.push) if self.push else False if not files and not ahead: self.logger.info("Nothing to commit or push.") diff --git a/devgen/modules/release_note_generator.py b/devgen/modules/release_note_generator.py index 56886ed..80948d7 100644 --- a/devgen/modules/release_note_generator.py +++ b/devgen/modules/release_note_generator.py @@ -12,11 +12,12 @@ class ReleaseNotesGenerator(ChangelogGenerator): "BREAKING CHANGES": "โš ๏ธ", "Features": "โœจ", "Bug Fixes": "๐Ÿ›", + "Performance": "โšก", "Documentation": "๐Ÿ“š", "Refactor": "โ™ป๏ธ", "Tests": "โœ…", "Chore": "๐Ÿ”ง", - "Style": "โœจ", + "Style": "๐Ÿ’„", "Other Changes": "๐Ÿงน", } @@ -32,7 +33,12 @@ class ReleaseNotesGenerator(ChangelogGenerator): "BREAKING CHANGES", "Features", "Bug Fixes", + "Performance", "Documentation", + "Refactor", + "Tests", + "Style", + "Chore", "Other Changes", ]