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
76 changes: 45 additions & 31 deletions src/git_draft/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import asyncio
import dataclasses
import enum
import importlib.metadata
import logging
import optparse
from pathlib import Path
import sys
from typing import Any

from .bots import load_bot
from .common import PROGRAM, Config, UnreachableError, ensure_state_home
Expand All @@ -28,7 +30,27 @@
_logger = logging.getLogger(__name__)


def new_parser() -> optparse.OptionParser:
class Accept(enum.Enum):
"""Valid change accept mode"""

MANUAL = 0
MERGE = enum.auto()
MERGE_THEIRS = enum.auto()
MERGE_THEN_QUIT = enum.auto()

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


def _new_parser() -> optparse.OptionParser:
parser = optparse.OptionParser(
prog=PROGRAM,
version=importlib.metadata.version("git_draft"),
Expand Down Expand Up @@ -94,39 +116,24 @@ def callback(
help="edit prompt or template",
action="store_true",
)

parser.add_option(
"--no-accept",
help="do not merge draft",
dest="accept",
action="store_const",
const=0,
)
parser.add_option(
"-f",
"--format",
dest="format",
help="formatting string",
)

return parser


class Accept(enum.Enum):
"""Valid change accept mode"""

MANUAL = 0
MERGE = enum.auto()
MERGE_THEIRS = enum.auto()
MERGE_THEN_QUIT = enum.auto()

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


def edit(*, path: Path | None = None, text: str | None = None) -> str:
def _edit(*, path: Path | None = None, text: str | None = None) -> str:
if sys.stdin.isatty():
return open_editor(text or "", path)
# We exit with a custom code to allow the caller to act accordingly.
Expand All @@ -145,12 +152,17 @@ def edit(*, path: Path | None = None, text: str | None = None) -> str:
sys.exit(199)


def _format(props: Any, spec: str) -> str:
"""Formats an instance of a dataclass using the provided pattern"""
return spec.format(**dataclasses.asdict(props))


_PROMPT_PLACEHOLDER = "Enter your prompt here..."


async def run() -> None: # noqa: PLR0912 PLR0915
config = Config.load()
(opts, args) = new_parser().parse_args()
(opts, args) = _new_parser().parse_args()

log_path = ensure_state_home() / "log"
if opts.log_path:
Expand Down Expand Up @@ -191,7 +203,7 @@ async def run() -> None: # noqa: PLR0912 PLR0915
prompt = TemplatedPrompt.public(args[0], args[1:])
editable = opts.edit
else:
prompt = edit(
prompt = _edit(
text=drafter.latest_draft_prompt() or _PROMPT_PLACEHOLDER
).strip()
if prompt.strip() == _PROMPT_PLACEHOLDER:
Expand All @@ -212,25 +224,27 @@ async def run() -> None: # noqa: PLR0912 PLR0915
drafter.quit_folio()
case "list-events":
draft_id = args[0] if args else None
for line in drafter.list_draft_events(draft_id):
print(line)
spec = opts.format or "{occurred_at}\t{description}"
for event_props in drafter.list_draft_events(draft_id):
print(_format(event_props, spec))
case "show-template":
if len(args) != 1:
raise ValueError("Expected exactly one argument")
name = args[0]
meta = find_prompt_metadata(name)
if opts.edit:
if meta:
edit(path=meta.local_path(), text=meta.source())
_edit(path=meta.local_path(), text=meta.source())
else:
edit(path=PromptMetadata.local_path_for(name))
_edit(path=PromptMetadata.local_path_for(name))
else:
if not meta:
raise ValueError(f"No template named {name!r}")
print(meta.source())
case "list-templates":
for line in list_templates():
print(line)
spec = opts.format or "{name}: {description}"
for template_props in list_templates():
print(_format(template_props, spec))
case _:
raise UnreachableError()

Expand Down
17 changes: 15 additions & 2 deletions src/git_draft/drafter.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,9 @@ def latest_draft_prompt(self) -> str | None:
prompt = "\n\n".join([prompt, reindent(question, prefix="> ")])
return prompt

def list_draft_events(self, draft_ref: str | None = None) -> Iterator[str]:
def list_draft_events(
self, draft_ref: str | None = None
) -> Iterator[DraftEventProperties]:
if draft_ref:
folio_id, seqno = _parse_draft_ref(draft_ref)
else:
Expand All @@ -469,7 +471,18 @@ def list_draft_events(self, draft_ref: str | None = None) -> Iterator[str]:
occurred_at, class_name, data = row
event = decoders[class_name].decode(data)
description = _format_event(event)
yield "\t".join([occurred_at, class_name, description])
yield DraftEventProperties(
occurred_at, class_name, description
)


@dataclasses.dataclass(frozen=True)
class DraftEventProperties:
"""Formattable properties corresponding to a draft's event"""

occurred_at: str
class_name: str
description: str


@dataclasses.dataclass(frozen=True)
Expand Down
17 changes: 14 additions & 3 deletions src/git_draft/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ def find_prompt_metadata(name: PromptName) -> PromptMetadata | None:
return prompt.metadata


def list_templates(*, include_local: bool = True) -> Iterator[str]:
def list_templates(
*, include_local: bool = True
) -> Iterator[TemplateProperties]:
env = _jinja_environment(include_local=include_local)
worktree = EmptyWorktree()
for rel_path in env.list_templates(extensions=[_extension]):
Expand All @@ -219,5 +221,14 @@ def list_templates(*, include_local: bool = True) -> Iterator[str]:
name, _ext = os.path.splitext(rel_path)
prompt = _load_prompt(env, name, worktree)
metadata = prompt.metadata
local = "y" if metadata.is_local() else "n"
yield "\t".join([name, local, metadata.description or ""])
scope = "local" if metadata.is_local() else "global"
yield TemplateProperties(name, scope, metadata.description or "")


@dataclasses.dataclass(frozen=True)
class TemplateProperties:
"""Prompt template formattable properties"""

name: str
scope: str
description: str