diff --git a/docs/git-draft.adoc b/docs/git-draft.adoc index 46b624b..f2126b3 100644 --- a/docs/git-draft.adoc +++ b/docs/git-draft.adoc @@ -18,15 +18,15 @@ 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 @@ -34,7 +34,7 @@ git draft [options] --show-templates [--json | [--edit] TEMPLATE] -a:: --accept:: --no-accept:: - Check out generated changes automatically. + Merge generated changes automatically. Can be repeated. -b BOT:: @@ -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. @@ -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:: diff --git a/src/git_draft/__main__.py b/src/git_draft/__main__.py index 3efa68c..c1d21f2 100644 --- a/src/git_draft/__main__.py +++ b/src/git_draft/__main__.py @@ -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", ) @@ -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", @@ -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, @@ -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): @@ -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()) @@ -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, @@ -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) diff --git a/src/git_draft/drafter.py b/src/git_draft/drafter.py index 47b99c8..76945d4 100644 --- a/src/git_draft/drafter.py +++ b/src/git_draft/drafter.py @@ -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 ) @@ -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. @@ -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, @@ -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") diff --git a/src/git_draft/toolbox.py b/src/git_draft/toolbox.py index 32a1854..632ecb0 100644 --- a/src/git_draft/toolbox.py +++ b/src/git_draft/toolbox.py @@ -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]() @@ -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: diff --git a/tests/git_draft/drafter_test.py b/tests/git_draft/drafter_test.py index a57112b..1a53909 100644 --- a/tests/git_draft/drafter_test.py +++ b/tests/git_draft/drafter_test.py @@ -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"