Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions docs/git-draft.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@ IMPORTANT: `git-draft` is WIP.
== Synopsis

[verse]
git draft [options] [--generate] [--accept... | --no-accept] [--bot BOT]
[--edit] [--reset | --no-reset] [TEMPLATE [VARIABLE...]]
git draft [options] --finalize
git draft [options] --show-templates [--json | [--edit] TEMPLATE]
git draft [options] [--new] [--accept... | --no-accept] [--bot BOT]
[--edit] [TEMPLATE [VARIABLE...]]
git draft [options] --quit
git draft [options] --templates [--json | [--edit] TEMPLATE]


== Description

`git-draft` is a git-centric way to develop using AI.
`git-draft` is a git-centric way to edit code with AI.


== Options

-a::
--accept::
--no-accept::
Check out generated changes automatically.
Merge generated changes automatically.
Can be repeated.

-b BOT::
Expand All @@ -43,18 +43,9 @@ git draft [options] --show-templates [--json | [--edit] TEMPLATE]

-e::
--edit::
Enable interactive editing of prompts and templates.
Enable interactive editing of draft prompts and templates.
See `--generate` and `--show-templates` for details.

-F::
--finalize::
Go back to the draft's origin branch with the current working directory.

-G::
--generate::
Add an AI-generated commit.
If the `--edit` option is set, an interactive editor will be open with the rendered prompt to allow modification before it is forwarded to the bot.

-h::
--help::
Show help message and exit.
Expand All @@ -66,12 +57,21 @@ git draft [options] --show-templates [--json | [--edit] TEMPLATE]
--log::
Show log path and exit.

-N::
--new::
Create an AI-generated draft.
If the `--edit` option is set, an interactive editor will be open with the rendered prompt to allow modification before it is forwarded to the bot.

-Q::
--quit::
Go back to the draft's origin branch with the current working directory.

--root::
Repository search root.

-T::
--show-templates::
Lists available templates.
--templates::
With no argument, lists available templates.
With an template name argument, displays the corresponding template's contents or, if the `--edit` option is set, opens an interactive editor.

--version::
Expand Down
51 changes: 29 additions & 22 deletions src/git_draft/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def new_parser() -> optparse.OptionParser:
parser.disable_interspersed_args()

parser.add_option(
"--log",
"--log-path",
help="show log path and exit",
action="store_true",
)
Expand Down Expand Up @@ -60,21 +60,21 @@ def callback(
**kwargs,
)

add_command("finalize", help="apply current draft to original branch")
add_command("generate", help="create or update draft from a prompt")
add_command("show-templates", short="T", help="show template information")
add_command("new", help="create a new draft from a prompt")
add_command("quit", help="return to original branch")
add_command("templates", short="T", help="show template information")

parser.add_option(
"-a",
"--accept",
help="accept draft, may be repeated",
help="merge draft, may be repeated",
action="count",
)
parser.add_option(
"-b",
"--bot",
dest="bot",
help="bot name",
help="AI bot name",
)
parser.add_option(
"-e",
Expand All @@ -91,7 +91,7 @@ def callback(

parser.add_option(
"--no-accept",
help="do not update worktree from draft",
help="do not merge draft",
dest="accept",
action="store_const",
const=0,
Expand All @@ -106,16 +106,18 @@ class Accept(enum.Enum):
MANUAL = 0
MERGE = enum.auto()
MERGE_THEIRS = enum.auto()
FINALIZE = enum.auto()
MERGE_THEN_QUIT = enum.auto()

def merge_strategy(self) -> DraftMergeStrategy | None:
match self.value:
match self:
case Accept.MANUAL:
return None
case Accept.MERGE:
return "ignore-all-space"
case _:
case Accept.MERGE_THEIRS | Accept.MERGE_THEN_QUIT:
return "theirs"
case _:
raise UnreachableError()


class ToolPrinter(ToolVisitor):
Expand Down Expand Up @@ -175,10 +177,15 @@ def main() -> None: # noqa: PLR0912 PLR0915
(opts, args) = new_parser().parse_args()

log_path = ensure_state_home() / "log"
if opts.log:
if opts.log_path:
print(log_path)
return
logging.basicConfig(level=config.log_level, filename=str(log_path))
logging.basicConfig(
level=config.log_level,
filename=str(log_path),
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
datefmt="%m-%d %H:%M",
)

repo = Repo.enclosing(Path(opts.root) if opts.root else Path.cwd())
drafter = Drafter.create(repo, Store.persistent())
Expand Down Expand Up @@ -210,7 +217,7 @@ def main() -> None: # noqa: PLR0912 PLR0915
prompt = sys.stdin.read()

accept = Accept(opts.accept or 0)
drafter.generate_draft(
draft = drafter.generate_draft(
prompt,
bot,
prompt_transform=open_editor if editable else None,
Expand All @@ -219,18 +226,18 @@ def main() -> None: # noqa: PLR0912 PLR0915
)
match accept:
case Accept.MANUAL:
print("Generated draft.")
print(f"Generated draft. [ref={draft.ref}].")
case Accept.MERGE | Accept.MERGE_THEIRS:
print("Merged draft.")
case Accept.FINALIZE:
drafter.finalize_folio()
print("Finalized draft.")
print(f"Generated and merged draft. [ref={draft.ref}]")
case Accept.MERGE_THEN_QUIT:
drafter.quit_folio()
print(f"Generated and applied draft. [ref={draft.ref}]")
case _:
raise UnreachableError()
case "finalize":
drafter.finalize_folio()
print("Finalized draft folio.")
case "show-templates":
case "quit":
drafter.quit_folio()
print("Applied draft.")
case "templates":
if args:
name = args[0]
tpl = find_template(name)
Expand Down
12 changes: 10 additions & 2 deletions src/git_draft/drafter.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ def generate_draft(
parent_commit_rev = self._commit_tree(
toolbox.tree_sha(), "HEAD", "sync(prompt)"
)
_logger.info("Created sync commit. [sha=%s]", parent_commit_rev)
else:
parent_commit_rev = "HEAD"
_logger.info("Skipping sync commit, tree is clean.")
commit_sha = self._record_change(
change, parent_commit_rev, folio, seqno
)
Expand Down Expand Up @@ -177,9 +179,10 @@ def generate_draft(
for o in operation_recorder.operations
],
)
_logger.info("Created new change in folio %s.", folio.id)
_logger.info("Created new draft in folio %s.", folio.id)

if merge_strategy:
_logger.info("Merging draft. [strategy=%s]", merge_strategy)
if parent_commit_rev != "HEAD":
# If there was a sync(prompt) commit, we move forward to it.
# This will avoid conflicts with changes that happened earlier.
Expand All @@ -194,6 +197,11 @@ def generate_draft(
"draft! merge",
commit_sha,
)
self._repo.git(
"update-ref",
f"refs/heads/{folio.upstream_branch_name()}",
"HEAD",
)

return Draft(
folio=folio,
Expand All @@ -203,7 +211,7 @@ def generate_draft(
token_count=change.action.token_count,
)

def finalize_folio(self) -> Folio:
def quit_folio(self) -> Folio:
folio = _active_folio(self._repo)
if not folio:
raise RuntimeError("Not currently on a draft branch")
Expand Down
5 changes: 3 additions & 2 deletions src/git_draft/toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ def __init__(

@classmethod
def for_working_dir(cls, repo: Repo) -> tuple[Self, bool]:
toolbox = cls(repo, "HEAD")
head_tree_sha = toolbox.tree_sha()
index_tree_sha = repo.git("write-tree").stdout
toolbox = cls(repo, index_tree_sha)

# Apply any changes from the working directory.
deleted = set[SHA]()
Expand All @@ -170,6 +170,7 @@ def for_working_dir(cls, repo: Repo) -> tuple[Self, bool]:
continue # Deleted files also show up as modified
toolbox._write_from_disk(PurePosixPath(path), path)

head_tree_sha = repo.git("rev-parse", "HEAD^{tree}").stdout
return toolbox, toolbox.tree_sha() != head_tree_sha

def with_visitors(self, visitors: Sequence[ToolVisitor]) -> Self:
Expand Down
4 changes: 2 additions & 2 deletions tests/git_draft/drafter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ def test_generate_reuse_branch(self) -> None:
def test_delete_unknown_file(self) -> None:
self._drafter.generate_draft("hello", _SimpleBot({"p1": None}))

def test_finalize_keeps_changes(self) -> None:
def test_quit_keeps_changes(self) -> None:
self._fs.write("p1.txt", "a1")
self._drafter.generate_draft("hello", _SimpleBot.prompt(), "theirs")
self._fs.write("p1.txt", "a2")
self._drafter.finalize_folio()
self._drafter.quit_folio()
assert self._fs.read("p1.txt") == "a2"
assert self._fs.read("PROMPT") == "hello"

Expand Down