import asyncio import os import sys from dotenv import load_dotenv from interpreter import OpenInterpreter load_dotenv() commit = {} submodule_diffs = {} def get_git_credentials(): if len(sys.argv) != 4: print("Usage: python gen_commit.py ") sys.exit(1) return sys.argv[1], sys.argv[2], sys.argv[3] def get_git_diff(project_path, summary=True): """Run git diff and return the output.""" flag = "--compact-summary" if summary else "" result = os.popen(f'cd "{project_path}" && git diff {flag}').read() staged_changes = os.popen(f'cd "{project_path}" && git diff --cached {flag}').read() # Combine both staged and unstaged changes all_changes = result.strip() + "\n" + staged_changes.strip() return all_changes.strip() if all_changes.strip() else "βœ“ No changes" def gen_commit(project_path): """Generate commit message using OpenInterpreter based on git diff output.""" agent = OpenInterpreter() agent.llm.model = "gpt-4o" agent.auto_run = True agent.verbose = False agent.llm.temperature = 0.3 convention_path = os.path.join( os.path.dirname(__file__), "resources/commit_convention.md" ) git_commit_convention_path = os.path.join( os.path.dirname(__file__), "resources/git_commit_convention.md" ) with open(convention_path, "r") as f: convention_content = f.read() with open(git_commit_convention_path, "r") as f: git_commit_convention_content = f.read() agent.system_message = f""" You are a helpful assistant that generates concise, conventional commit messages based on code diffs. Guidelines: - Use English. - Do not include explanationsβ€”only output commit message in under 100 characters. - Use this git commit convention to get uncommitted changes: {git_commit_convention_content} - If there are no uncommitted changes, return "No changes" - After getting uncommitted changes, analyze and generate a commit message Follow this convention: {convention_content} - Commit Message should be in under 100 characters. """ prompt = f""" Analyze the git commit and generate a commit message for this project: {project_path} """ response = agent.chat(prompt) if ( isinstance(response, list) and response and isinstance(response[-1], dict) and "content" in response[-1] ): return response[-1]["content"].strip() return str(response).strip() async def has_submodules(): """Check if the repository has any submodules.""" proc = await asyncio.create_subprocess_exec( "git", "config", "--file", ".gitmodules", "--get-regexp", r"^submodule\.", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() return bool(stdout.decode().strip()) async def get_submodule_info(): if not await has_submodules(): return {} proc = await asyncio.create_subprocess_exec( "git", "config", "--file", ".gitmodules", "--get-regexp", r"^submodule\..*\.path$", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if stderr: print(f"Error getting submodule paths:\n{stderr.decode()}") return {} submodule_info = {} for line in stdout.decode().splitlines(): parts = line.split() if len(parts) == 2: name = parts[0].split(".")[1] path = parts[1] abs_path = os.path.abspath(path) submodule_info[abs_path] = name return submodule_info async def commit_and_push_submodules(): if not await has_submodules(): print("No submodules found in this repository.") return print("Checking for submodule changes...") submodule_info = await get_submodule_info() proc = await asyncio.create_subprocess_shell( "git submodule foreach --quiet 'if [ -n \"$(git status --porcelain)\" ]; then echo $path; fi'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if stderr: print(f"Error checking submodules:\n{stderr.decode()}") return submodule_paths = stdout.decode().strip().split("\n") git_user, git_pass, _ = get_git_credentials() for submodule_path in submodule_paths: if not submodule_path: continue abs_path = os.path.abspath(submodule_path) name = submodule_info.get(abs_path, submodule_path) print(f"Processing submodule: {name} at {abs_path}") commit_msg = gen_commit(abs_path) commit[abs_path] = commit_msg if commit_msg == "No changes": print(f"No changes for {name} at {abs_path}") continue proc = await asyncio.create_subprocess_exec( "git", "remote", "get-url", "origin", cwd=abs_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if stderr: print(f"Error getting remote URL for {name}:\n{stderr.decode()}") continue remote_url = stdout.decode().strip().replace(".git", "").replace("https://", "") remote_url = f"https://{git_user}:{git_pass}@{remote_url}" commands = [ f"cd {abs_path} && git add .", f'cd {abs_path} && git commit -m "{commit_msg}" || true', f"cd {abs_path} && git push {remote_url} || true", ] for cmd in commands: proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if stdout: print(stdout.decode()) if stderr: print(f"Submodule '{name}' stderr:\n{stderr.decode()}") def format_tree_output(project_path, base_name, base_diff, submodule_diffs, commit): """Format the output in a tree structure.""" output = [f"πŸ“ {base_name}"] output.append(f" └── πŸ“ {commit[project_path]}:") output.append(f" └── πŸ“¦ {base_diff}") if submodule_diffs: output.append("πŸ“š Submodules:") for submodule_path, diff in submodule_diffs.items(): name = os.path.basename(submodule_path) output.append(f" └── πŸ“¦ {name}") if submodule_path in commit: output.append(f" └── πŸ“ {commit[submodule_path]}:") for line in diff.split("\n"): output.append(f" └── {line}") return "\n".join(output) async def push_code(): git_user, git_pass, project_path = get_git_credentials() os.chdir(project_path) print(f"Changed to project path: {project_path}") base_name = os.path.basename(project_path) base_diff = get_git_diff(project_path, True) if await has_submodules(): submodule_info = await get_submodule_info() for path in submodule_info: submodule_diffs[path] = get_git_diff(path) # Rest of the push_code function remains the same proc = await asyncio.create_subprocess_exec( "git", "remote", "get-url", "origin", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() if stderr: print(f"Failed to get remote URL: {stderr.decode()}") git_repo = stdout.decode().strip().replace(".git", "").replace("https://", "") remote_url = f"https://{git_user}:{git_pass}@{git_repo}" await commit_and_push_submodules() commit_msg = gen_commit(project_path) commit[project_path] = commit_msg print(f"Generated commit message:\n{commit_msg}\n") commands = [ f"git pull {remote_url} || true", "git add .", f'git commit -m "{commit_msg}" || true', f"git push {remote_url} || true", ] for cmd in commands: proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() if proc.returncode != 0: print(f"Command failed: {cmd}") print(stderr.decode()) elif stderr: # Not an error; just info output print(stderr.decode()) # Format and print the tree output tree_output = format_tree_output( project_path, base_name, base_diff, submodule_diffs, commit ) print("\nProject Status:") print(tree_output) print() def confirm_commit(project_path, base_name, base_diff): tree_output = format_tree_output( project_path, base_name, base_diff, submodule_diffs, commit ) print("\nProject Status:") print(tree_output) print() confirmation = input( "Do you want to commit and push these changes? (y/n): " ).lower() return confirmation in ["y", "yes"] if __name__ == "__main__": asyncio.run(push_code())