Skip to content

Commit 698fe8e

Browse files
authored
feat: support editing templates (#48)
1 parent 8d297f4 commit 698fe8e

File tree

8 files changed

+294
-173
lines changed

8 files changed

+294
-173
lines changed

docs/git-draft.adoc

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,98 @@ v{manversion}
1212

1313
git-draft - git-friendly code assistant
1414

15-
IMPORTANT: _git-draft_ is WIP.
16-
Options documented below may not be implemented yet.
15+
IMPORTANT: `git-draft` is WIP.
1716

1817

1918
== Synopsis
2019

21-
*git-draft* _[--generate]_ _[--prompt PROMPT]_ _[--reset]_ _[TEMPLATE [...]]_
20+
[verse]
21+
git draft [options] [--generate] [--bot BOT] [--edit] [--reset | --no-reset]
22+
[--sync] [TEMPLATE [VARIABLE...]]
23+
git draft [options] --finalize [--clean | --revert] [--delete]
24+
git draft [options] --show-drafts [--json]
25+
git draft [options] --show-prompts [--json] [PROMPT]
26+
git draft [options] --show-templates [--json | [--edit] TEMPLATE]
2227

23-
*git-draft* _--finalize_ _[--delete]_
2428

25-
*git-draft* _--discard_ _[--delete]_
29+
== Description
2630

31+
`git-draft` is a git-centric way to edit code using AI.
2732

28-
== Description
2933

30-
_git-draft_ is a git-centric way to edit code using AI.
34+
== Options
35+
36+
-b BOT::
37+
--bot=BOT::
38+
Bot name.
39+
40+
-c::
41+
--clean::
42+
TODO
43+
44+
-d::
45+
--delete::
46+
Delete finalized branch.
47+
48+
-e::
49+
--edit::
50+
Edit.
51+
52+
-F::
53+
--finalize::
54+
TODO
55+
56+
-G::
57+
--generate::
58+
TODO
59+
60+
-h::
61+
--help::
62+
Show help message and exit.
63+
64+
-j::
65+
--json::
66+
Use JSON output.
67+
68+
--log::
69+
Show log path and exit.
70+
71+
--reset::
72+
--no-reset::
73+
TODO
74+
75+
-r::
76+
--revert::
77+
Abandon any changes since draft creation.
78+
79+
--root::
80+
Repository search root.
81+
82+
-D::
83+
--show-drafts::
84+
TODO
85+
86+
-P::
87+
--show-prompts::
88+
TODO
89+
90+
-T::
91+
--show-templates::
92+
TODO
93+
94+
-s::
95+
--sync::
96+
Create a sync commit with any changes.
97+
98+
-t TIMEOUT::
99+
--timeout=TIMEOUT::
100+
Action timeout.
101+
102+
--version::
103+
Show version and exit.
31104

32-
=== How it works
33105

106+
== Examples
34107

35108
The workhorse command is `git draft --generate` which leverages AI to edit our code.
36109
A prompt can be specified as standard input, for example `echo "Add a test for compute_offset in chart.py" | git draft --generate`.
@@ -52,4 +125,4 @@ Note that you can come back to an existing draft anytime (by checking its branch
52125

53126
== See also
54127

55-
_git(1)_
128+
`git(1)`

src/git_draft/__main__.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
import importlib.metadata
66
import logging
77
import optparse
8-
from pathlib import PurePosixPath
8+
from pathlib import Path, PurePosixPath
99
import sys
1010
from typing import Sequence
1111

1212
from .bots import load_bot
1313
from .common import PROGRAM, Config, UnreachableError, ensure_state_home
1414
from .drafter import Drafter
1515
from .editor import open_editor
16-
from .prompt import TemplatedPrompt, template_source, templates_table
16+
from .prompt import Template, TemplatedPrompt, templates_table
1717
from .store import Store
1818
from .toolbox import ToolVisitor
1919

@@ -54,9 +54,9 @@ def callback(_option, _opt, _value, parser) -> None:
5454

5555
add_command("finalize", help="apply current draft to original branch")
5656
add_command("generate", help="start a new draft from a prompt")
57-
add_command("history", help="show history drafts or prompts")
58-
add_command("revert", help="discard the current draft")
59-
add_command("templates", help="show template information")
57+
add_command("show-drafts", short="D", help="show draft history")
58+
add_command("show-prompts", short="P", help="show prompt history")
59+
add_command("show-templates", short="T", help="show template information")
6060

6161
parser.add_option(
6262
"-b",
@@ -76,7 +76,12 @@ def callback(_option, _opt, _value, parser) -> None:
7676
help="delete draft after finalizing or discarding",
7777
action="store_true",
7878
)
79-
# TODO: Add edit option. Works both for prompts and templates.
79+
parser.add_option(
80+
"-e",
81+
"--edit",
82+
help="edit prompt or template",
83+
action="store_true",
84+
)
8085
parser.add_option(
8186
"-j",
8287
"--json",
@@ -85,8 +90,8 @@ def callback(_option, _opt, _value, parser) -> None:
8590
)
8691
parser.add_option(
8792
"-r",
88-
"--reset",
89-
help="reset index before generating a new draft",
93+
"--revert",
94+
help="abandon any changes since draft creation",
9095
action="store_true",
9196
)
9297
parser.add_option(
@@ -95,11 +100,23 @@ def callback(_option, _opt, _value, parser) -> None:
95100
help="commit prior worktree changes separately",
96101
action="store_true",
97102
)
103+
104+
parser.add_option(
105+
"--no-reset",
106+
help="abort if there are any staged changes",
107+
dest="reset",
108+
action="store_false",
109+
)
110+
parser.add_option(
111+
"--reset",
112+
help="reset index before generating a new draft",
113+
dest="reset",
114+
action="store_true",
115+
)
98116
parser.add_option(
99-
"-t",
100117
"--timeout",
101118
dest="timeout",
102-
help="bot generation timeout",
119+
help="generation timeout",
103120
)
104121

105122
return parser
@@ -125,6 +142,17 @@ def on_delete_file(self, path: PurePosixPath, _reason: str | None) -> None:
125142
print(f"Deleted {path}.")
126143

127144

145+
def edit(text: str | None, path: Path | None) -> str | None:
146+
if sys.stdin.isatty():
147+
return open_editor(text or "", path)
148+
else:
149+
if path and text is not None:
150+
with open(path, "w") as f:
151+
f.write(text)
152+
print(path)
153+
return None
154+
155+
128156
def main() -> None:
129157
config = Config.load()
130158
(opts, args) = new_parser().parse_args()
@@ -162,22 +190,35 @@ def main() -> None:
162190
bot,
163191
bot_name=opts.bot,
164192
tool_visitors=[ToolPrinter()],
165-
reset=opts.reset,
193+
reset=config.auto_reset if opts.reset is None else opts.reset,
194+
sync=opts.sync,
166195
)
167196
print(f"Generated {name}.")
168197
elif command == "finalize":
169-
name = drafter.finalize_draft(clean=opts.clean, delete=opts.delete)
170-
print(f"Finalized {name}.")
171-
elif command == "revert":
172-
name = drafter.revert_draft(delete=opts.delete)
173-
print(f"Reverted {name}.")
174-
elif command == "history":
198+
name = drafter.exit_draft(
199+
revert=opts.revert, clean=opts.clean, delete=opts.delete
200+
)
201+
verb = "Reverted" if opts.revert else "Finalized"
202+
print(f"{verb} {name}.")
203+
elif command == "show-drafts":
175204
table = drafter.history_table(args[0] if args else None)
176205
if table:
177206
print(table.to_json() if opts.json else table)
178-
elif command == "templates":
207+
elif command == "show-prompts":
208+
raise NotImplementedError() # TODO
209+
elif command == "show-templates":
179210
if args:
180-
print(template_source(args[0]))
211+
name = args[0]
212+
tpl = Template.find(name)
213+
if opts.edit:
214+
if tpl:
215+
edit(tpl.source, tpl.local_path())
216+
else:
217+
edit("", Template.local_path_for(name))
218+
else:
219+
if not tpl:
220+
raise ValueError(f"No template named {name!r}")
221+
print(tpl.source)
181222
else:
182223
table = templates_table()
183224
print(table.to_json() if opts.json else table)

src/git_draft/common.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,28 @@ def ensure_state_home() -> Path:
3535

3636
@dataclasses.dataclass(frozen=True)
3737
class Config:
38-
log_level: int
39-
bots: Sequence[BotConfig]
38+
log_level: int = logging.INFO
39+
auto_reset: bool = True
40+
bots: Sequence[BotConfig] = dataclasses.field(default_factory=lambda: [])
4041

4142
@staticmethod
4243
def folder_path() -> Path:
4344
return xdg_base_dirs.xdg_config_home() / PROGRAM
4445

45-
@classmethod
46-
def default(cls) -> Self:
47-
return cls(logging.INFO, [])
48-
4946
@classmethod
5047
def load(cls) -> Self:
5148
path = cls.folder_path() / "config.toml"
5249
try:
5350
with open(path, "rb") as reader:
5451
data = tomllib.load(reader)
5552
except FileNotFoundError:
56-
return cls.default()
53+
return cls()
5754
else:
58-
return cls(
59-
log_level=logging.getLevelName(data["log_level"]),
60-
bots=[BotConfig(**v) for v in data.get("bots", [])],
61-
)
55+
if level := data["log_level"]:
56+
data["log_level"] = logging.getLevelName(level)
57+
if bots := data["bots"]:
58+
data["bots"] = [BotConfig(**v) for v in bots]
59+
return cls(**data)
6260

6361

6462
@dataclasses.dataclass(frozen=True)

0 commit comments

Comments
 (0)