gabriel / muse public
feat patch task/format-patch #1 / 1
gabriel · 166 days ago · Apr 14, 2026 · Diff

feat: implement muse format-patch — export commits as .patch files

- _make_patch_filename: numbered (0001-...), filesystem-safe subject sanitization (strips control chars, replaces /\..*?:<>|, collapses hyphens, truncates to 52 chars + extension) - _format_patch_content: mail-format headers (Date, Subject [PATCH N/M], X-Muse-Commit-ID) + unified diff for each changed file, sorted by path - _resolve_commit_range: resolves HEAD/branch/commit + -N shorthand by walking the parent_commit_id chain N steps back; returns oldest-first - run: resolves range → formats each patch → writes to files or --stdout; --json emits {patches: [{file, commit_id, subject}]} - 17 tests: unit (filename numbering, sanitization, content headers/diff lines), integration (single commit, numbered filename, muse header, diff content, --stdout, --json schema, initial commit no-parent, empty repo error), security (malicious subject → safe filename), stress (-9 flag produces 9 patch files)

sha256:5251840b865e589605e4740cf8c4649ee3d1cf79ca3ba315fc27f1a6483c009b sha
+70 ~29 symbols
sha256:7b442e21ec2bbbc0da9238227916158d63dfa91d23ddc0b675eea40dee206151 snapshot
+70
symbols added
~29
symbols modified
0
dead code introduced
Semantic Changes 99 symbols
~ muse/cli/commands/format_patch.py .py 28 symbols added
+ _MAX_SUBJECT_LEN variable variable _MAX_SUBJECT_LEN L88–88
+ _format_patch_content function function _format_patch_content L122–195
+ _make_patch_filename function function _make_patch_filename L96–119
+ _resolve_commit_range function function _resolve_commit_range L198–263
+ ExitCode import import ExitCode L72–72
+ Manifest import import Manifest L83–83
+ _json import import _json L66–66
+ annotations import import annotations L61–61
+ argparse import import argparse L63–63
+ contain_path import import contain_path L82–82
+ datetime import import datetime L64–64
+ difflib import import difflib L65–65
+ get_head_commit_id import import get_head_commit_id L75–75
+ logging import import logging L67–67
+ pathlib import import pathlib L68–68
+ re import import re L69–69
+ read_commit import import read_commit L75–75
+ read_current_branch import import read_current_branch L75–75
+ read_object import import read_object L73–73
+ read_repo_id import import read_repo_id L74–74
+ read_snapshot import import read_snapshot L75–75
+ require_repo import import require_repo L74–74
+ resolve_commit_ref import import resolve_commit_ref L75–75
+ sanitize_display import import sanitize_display L82–82
+ sys import import sys L70–70
+ logger variable variable logger L85–85
+ register function function register L271–310
+ run function function run L318–411
~ tests/test_cmd_format_patch.py .py 41 symbols added
+ _REPO_ID variable variable _REPO_ID L31–31
+ _commit_files function function _commit_files L59–98
+ _counter variable variable _counter L32–32
+ _env function function _env L55–56
+ _init_repo function function _init_repo L44–52
+ _invoke function function _invoke L101–103
+ _sha function function _sha L40–41
+ CliRunner import import CliRunner L23–23
+ CommitRecord import import CommitRecord L26–26
+ Manifest import import Manifest L27–27
+ SnapshotRecord import import SnapshotRecord L26–26
+ annotations import import annotations L13–13
+ compute_commit_id import import compute_commit_id L25–25
+ compute_snapshot_id import import compute_snapshot_id L25–25
+ datetime import import datetime L15–15
+ hashlib import import hashlib L16–16
+ json import import json L17–17
+ os import import os L18–18
+ pathlib import import pathlib L19–19
+ pytest import import pytest L21–21
+ write_commit import import write_commit L26–26
+ write_object import import write_object L24–24
+ write_snapshot import import write_snapshot L26–26
+ runner variable variable runner L29–29
+ test_format_patch_10_commits_range function function test_format_patch_10_commits_range L340–355
+ test_format_patch_content_has_required_headers function function test_format_patch_content_has_required_headers L151–169
+ test_format_patch_content_includes_diff_lines function function test_format_patch_content_includes_diff_lines L172–189
+ test_format_patch_empty_repo_exits_nonzero function function test_format_patch_empty_repo_exits_nonzero L287–290
+ test_format_patch_file_has_diff_content function function test_format_patch_file_has_diff_content L233–242
+ test_format_patch_file_has_muse_commit_id_header function function test_format_patch_file_has_muse_commit_id_header L220–230
+ test_format_patch_initial_commit_no_parent function function test_format_patch_initial_commit_no_parent L298–309
+ test_format_patch_json_schema function function test_format_patch_json_schema L265–279
+ test_format_patch_malicious_subject_safe_filename function function test_format_patch_malicious_subject_safe_filename L317–332
+ test_format_patch_single_commit_creates_file function function test_format_patch_single_commit_creates_file L197–206
+ test_format_patch_single_commit_numbered_filename function function test_format_patch_single_commit_numbered_filename L209–217
+ test_format_patch_stdout_contains_patch_content function function test_format_patch_stdout_contains_patch_content L250–257
+ test_make_patch_filename_high_number function function test_make_patch_filename_high_number L118–121
+ test_make_patch_filename_max_length function function test_make_patch_filename_max_length L139–143
+ test_make_patch_filename_numbered function function test_make_patch_filename_numbered L111–115
+ test_make_patch_filename_sanitizes_subject function function test_make_patch_filename_sanitizes_subject L124–129
+ test_make_patch_filename_strips_control_chars function function test_make_patch_filename_strips_control_chars L132–136
← Older Oldest on task/format-patch
All commits
Newer → Latest on task/format-patch

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:5251840b865e589605e4740cf8c4649ee3d1cf79ca3ba315fc27f1a6483c009b --body "your comment"