From 0867121734b9075be8f09f91889deb7291d61e75 Mon Sep 17 00:00:00 2001 From: YongxinFu <190456533+YongxinFu@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:52:20 +0800 Subject: [PATCH] feat: save markdown output to a file Assisted-by: Codex (OpenAI) --- README.md | 2 +- src/diffnote/cli.py | 18 ++++++++++++++++-- tests/test_cli.py | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2b6200b..120efd7 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ In any git repository with changes: diffnote # pretty terminal summary of your working tree diffnote --staged # only staged changes diffnote --markdown # GitHub PR-body markdown (paste-ready) +diffnote --markdown --save out.md # write the PR body to a file, without stdout output diffnote --commits 5 # summarize the last 5 commits diffnote --base main # changes vs main (great for PR prep) diffnote --json # machine-readable, for CI @@ -134,7 +135,6 @@ and then only to `localhost` (your Ollama). ## Roadmap -- [ ] `--save` to write markdown straight into a PR template - [ ] Breaking-change detection (`feat!:` / public API paths) - [ ] `--stats` per-area totals - [ ] Plugin hooks for custom risk rules diff --git a/src/diffnote/cli.py b/src/diffnote/cli.py index 20961be..b4f1171 100644 --- a/src/diffnote/cli.py +++ b/src/diffnote/cli.py @@ -28,6 +28,8 @@ def build_parser(): fmt = p.add_argument_group("output") fmt.add_argument("--markdown", action="store_true", help="Print a GitHub PR-body markdown.") + fmt.add_argument("--save", metavar="PATH", + help="Write markdown output to PATH instead of stdout (requires --markdown).") fmt.add_argument("--json", action="store_true", help="Print machine-readable JSON.") fmt.add_argument("--no-color", action="store_true", @@ -51,7 +53,10 @@ def get_diff(args, cwd): def main(argv=None): - args = build_parser().parse_args(argv) + parser = build_parser() + args = parser.parse_args(argv) + if args.save and (not args.markdown or args.json): + parser.error("--save requires --markdown and cannot be used with --json") try: cwd = gitutil.repo_root() except gitutil.GitError as exc: @@ -85,7 +90,16 @@ def main(argv=None): if args.json: print(render.render_json(summary, llm_prose)) elif args.markdown: - print(render.render_markdown(summary, llm_prose)) + markdown = render.render_markdown(summary, llm_prose) + if args.save: + try: + with open(args.save, "w", encoding="utf-8", newline="\n") as output_file: + output_file.write(markdown + "\n") + except OSError as exc: + print(f"diffnote: could not write {args.save}: {exc}", file=sys.stderr) + return 1 + else: + print(markdown) else: print(render.render_terminal(summary, use_color=not args.no_color, llm_prose=llm_prose)) return 0 diff --git a/tests/test_cli.py b/tests/test_cli.py index 1cc13d6..652ec8a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -72,6 +72,25 @@ def test_markdown_output(capsys): assert "## Changes" in out +def test_save_markdown_writes_file_without_stdout(capsys): + d = _make_dirty_repo() + output_path = os.path.join(d, "pr-body.md") + old = os.getcwd() + os.chdir(d) + try: + rc = cli.main(["--save", output_path, "--markdown"]) + finally: + os.chdir(old) + captured = capsys.readouterr() + with open(output_path, encoding="utf-8") as output_file: + saved = output_file.read() + assert rc == 0 + assert captured.out == "" + assert "## Summary" in saved + assert "## Changes" in saved + assert saved.endswith("\n") + + def test_no_changes(capsys): import os d = tempfile.mkdtemp()