Skip to content

Commit 4ba6560

Browse files
authored
Add new sample: Gitty (#5842)
1 parent 8f737de commit 4ba6560

File tree

12 files changed

+3308
-1
lines changed

12 files changed

+3308
-1
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,7 @@ notebook/coding
198198
artifacts
199199

200200
# project data
201-
registry.json
201+
registry.json
202+
203+
# files created by the gitty agent in python/samples/gitty
204+
.gitty/

python/samples/gitty/.python-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

python/samples/gitty/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) Microsoft Corporation.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE

python/samples/gitty/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# gitty (Warning: WIP)
2+
3+
This is an AutoGen powered CLI that generates draft replies for issues and pull requests
4+
to reduce maintenance overhead for open source projects.
5+
6+
Simple installation and CLI:
7+
8+
```bash
9+
gitty --repo microsoft/autogen issue 5212
10+
```
11+
12+
*Important*: Install the dependencies and set OpenAI API key:
13+
14+
```bash
15+
uv sync --all-extras
16+
source .venv/bin/activate
17+
export OPENAI_API_KEY=sk-....
18+
```

python/samples/gitty/pyproject.toml

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "gitty"
7+
version = "0.1.0"
8+
license = {file = "LICENSE"}
9+
description = "A Python project for GitHub issue content retrieval and user interaction."
10+
readme = "README.md"
11+
requires-python = ">=3.10"
12+
classifiers = [
13+
"Programming Language :: Python :: 3",
14+
"License :: OSI Approved :: MIT License",
15+
"Operating System :: OS Independent",
16+
]
17+
dependencies = [
18+
"aiohttp>=3.7.4",
19+
"pyperclip>=1.8.2",
20+
"autogen_agentchat>=0.4.3,<0.5.0",
21+
"autogen_ext[openai]>=0.4.3,<0.5.0",
22+
"rich>=13.0.0",
23+
"chromadb"
24+
]
25+
26+
[project.scripts]
27+
gitty = "gitty:main"
28+
29+
[dependency-groups]
30+
dev = [
31+
"poethepoet",
32+
"mypy",
33+
"pyright",
34+
"ruff"
35+
]
36+
37+
[tool.ruff]
38+
line-length = 120
39+
fix = true
40+
41+
target-version = "py310"
42+
43+
[tool.ruff.format]
44+
docstring-code-format = true
45+
46+
[tool.ruff.lint]
47+
select = ["E", "F", "W", "B", "Q", "I", "ASYNC", "T20"]
48+
ignore = ["F401", "E501"]
49+
50+
[tool.mypy]
51+
strict = true
52+
python_version = "3.10"
53+
ignore_missing_imports = true
54+
55+
# from https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/
56+
disallow_untyped_defs = true
57+
no_implicit_optional = true
58+
check_untyped_defs = true
59+
warn_return_any = true
60+
show_error_codes = true
61+
warn_unused_ignores = false
62+
63+
disallow_incomplete_defs = true
64+
disallow_untyped_decorators = true
65+
disallow_any_unimported = true
66+
67+
[tool.pyright]
68+
include = ["src", "tests"]
69+
typeCheckingMode = "strict"
70+
reportUnnecessaryIsInstance = false
71+
reportMissingTypeStubs = false
72+
73+
[tool.poe.tasks]
74+
mypy = "mypy ."
75+
pyright = "pyright"
76+
format = "ruff format"
77+
lint = "ruff check"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .__main__ import main
2+
3+
__all__ = ["main"]
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import argparse
2+
import asyncio
3+
import os
4+
import subprocess
5+
import sys
6+
from rich.console import Console
7+
8+
from ._gitty import run_gitty, get_gitty_dir
9+
from ._db import fetch_and_update_issues
10+
11+
console = Console()
12+
13+
def check_openai_key() -> None:
14+
"""Check if OpenAI API key is set in environment variables."""
15+
if not os.getenv("OPENAI_API_KEY"):
16+
print("Error: OPENAI_API_KEY environment variable is not set.")
17+
print("Please set your OpenAI API key using:")
18+
print(" export OPENAI_API_KEY='your-api-key'")
19+
sys.exit(1)
20+
21+
22+
def check_gh_cli() -> bool:
23+
"""Check if GitHub CLI is installed and accessible."""
24+
try:
25+
subprocess.run(["gh", "--version"], capture_output=True, check=True)
26+
return True
27+
except (subprocess.CalledProcessError, FileNotFoundError):
28+
print("[error]Error: GitHub CLI (gh) is not installed or not found in PATH.[/error]")
29+
print("Please install it from: https://cli.github.com")
30+
sys.exit(1)
31+
32+
33+
def edit_config_file(file_path: str) -> None:
34+
if not os.path.exists(file_path):
35+
with open(file_path, "w") as f:
36+
f.write("# Instructions for gitty agents\n")
37+
f.write("# Add your configuration below\n")
38+
editor = os.getenv("EDITOR", "vi")
39+
subprocess.run([editor, file_path])
40+
41+
42+
def main() -> None:
43+
parser = argparse.ArgumentParser(
44+
description="Gitty: A GitHub Issue/PR Assistant.\n\n"
45+
"This tool fetches GitHub issues or pull requests and uses an AI assistant to generate concise,\n"
46+
"technical responses to help make progress on your project. You can specify a repository using --repo\n"
47+
"or let the tool auto-detect the repository based on the current directory.",
48+
epilog="Subcommands:\n issue - Process and respond to GitHub issues\n pr - Process and respond to GitHub pull requests\n local - Edit repo-specific gitty config\n global- Edit global gitty config\n\n"
49+
"Usage examples:\n gitty issue 123\n gitty pr 456\n gitty local\n gitty global",
50+
formatter_class=argparse.RawTextHelpFormatter,
51+
)
52+
parser.add_argument(
53+
"command", choices=["issue", "pr", "fetch", "local", "global"], nargs="?", help="Command to execute"
54+
)
55+
parser.add_argument("number", type=int, nargs="?", help="Issue or PR number (if applicable)")
56+
57+
if len(sys.argv) == 1:
58+
parser.print_help()
59+
sys.exit(0)
60+
61+
args = parser.parse_args()
62+
command = args.command
63+
64+
# Check for gh CLI installation before processing commands that need it
65+
if command in ["issue", "pr", "fetch"]:
66+
check_gh_cli()
67+
68+
# Check for OpenAI API key before processing commands that need it
69+
if command in ["issue", "pr"]:
70+
check_openai_key()
71+
72+
if command in ["issue", "pr"]:
73+
# Always auto-detect repository
74+
pipe = subprocess.run(
75+
[
76+
"gh",
77+
"repo",
78+
"view",
79+
"--json",
80+
"owner,name",
81+
"-q",
82+
'.owner.login + "/" + .name',
83+
],
84+
check=True,
85+
capture_output=True,
86+
)
87+
owner, repo = pipe.stdout.decode().strip().split("/")
88+
number = args.number
89+
if command == "issue":
90+
asyncio.run(run_gitty(owner, repo, command, number))
91+
else:
92+
print(f"Command '{command}' is not implemented.")
93+
sys.exit(1)
94+
elif command == "fetch":
95+
pipe = subprocess.run(
96+
[
97+
"gh",
98+
"repo",
99+
"view",
100+
"--json",
101+
"owner,name",
102+
"-q",
103+
'.owner.login + "/" + .name',
104+
],
105+
check=True,
106+
capture_output=True,
107+
)
108+
owner, repo = pipe.stdout.decode().strip().split("/")
109+
gitty_dir = get_gitty_dir()
110+
db_path = os.path.join(gitty_dir, "issues.db")
111+
fetch_and_update_issues(owner, repo, db_path)
112+
elif command == "local":
113+
gitty_dir = get_gitty_dir()
114+
local_config_path = os.path.join(gitty_dir, "config")
115+
edit_config_file(local_config_path)
116+
elif command == "global":
117+
global_config_dir = os.path.expanduser("~/.gitty")
118+
os.makedirs(global_config_dir, exist_ok=True)
119+
global_config_path = os.path.join(global_config_dir, "config")
120+
edit_config_file(global_config_path)
121+
122+
if __name__ == "__main__":
123+
main()
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
import subprocess
3+
import sys
4+
from rich.theme import Theme
5+
6+
os.environ["TOKENIZERS_PARALLELISM"] = "false" # disable parallelism to avoid warning
7+
8+
custom_theme = Theme(
9+
{
10+
"header": "bold",
11+
"thinking": "italic yellow",
12+
"acting": "italic red",
13+
"prompt": "italic",
14+
"observe": "italic",
15+
"success": "bold green",
16+
}
17+
)
18+
19+
def get_repo_root() -> str:
20+
try:
21+
result = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=True)
22+
return result.stdout.strip()
23+
except subprocess.CalledProcessError:
24+
print("Error: not a git repository.")
25+
sys.exit(1)
26+
27+
28+
def get_gitty_dir() -> str:
29+
"""Get the .gitty directory in the repository root. Create it if it doesn't exist."""
30+
repo_root = get_repo_root()
31+
gitty_dir = os.path.join(repo_root, ".gitty")
32+
if not os.path.exists(gitty_dir):
33+
os.makedirs(gitty_dir)
34+
return gitty_dir

0 commit comments

Comments
 (0)