mseep-patche 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mseep_patche-1.0.1/LICENSE +21 -0
- mseep_patche-1.0.1/PKG-INFO +14 -0
- mseep_patche-1.0.1/pyproject.toml +45 -0
- mseep_patche-1.0.1/src/Patche/__init__.py +1 -0
- mseep_patche-1.0.1/src/Patche/__main__.py +3 -0
- mseep_patche-1.0.1/src/Patche/__version__.py +9 -0
- mseep_patche-1.0.1/src/Patche/app.py +32 -0
- mseep_patche-1.0.1/src/Patche/commands/apply.py +107 -0
- mseep_patche-1.0.1/src/Patche/commands/help.py +12 -0
- mseep_patche-1.0.1/src/Patche/commands/show.py +41 -0
- mseep_patche-1.0.1/src/Patche/config.py +24 -0
- mseep_patche-1.0.1/src/Patche/mcp/__init__.py +42 -0
- mseep_patche-1.0.1/src/Patche/mcp/__main__.py +3 -0
- mseep_patche-1.0.1/src/Patche/mcp/model.py +29 -0
- mseep_patche-1.0.1/src/Patche/mcp/prompts.py +313 -0
- mseep_patche-1.0.1/src/Patche/mcp/server.py +101 -0
- mseep_patche-1.0.1/src/Patche/mcp/tools.py +160 -0
- mseep_patche-1.0.1/src/Patche/model.py +75 -0
- mseep_patche-1.0.1/src/Patche/utils/common.py +73 -0
- mseep_patche-1.0.1/src/Patche/utils/header.py +34 -0
- mseep_patche-1.0.1/src/Patche/utils/parse.py +269 -0
- mseep_patche-1.0.1/src/Patche/utils/resolve.py +168 -0
- mseep_patche-1.0.1/tests/bench_parse.py +129 -0
- mseep_patche-1.0.1/tests/cases/904d88-email.patch +52 -0
- mseep_patche-1.0.1/tests/cases/904d88-git.patch +45 -0
- mseep_patche-1.0.1/tests/cases/923936.patch +33 -0
- mseep_patche-1.0.1/tests/cases/abc +1 -0
- mseep_patche-1.0.1/tests/cases/diff-unified.diff +19 -0
- mseep_patche-1.0.1/tests/cases/diff-unified2.diff +5 -0
- mseep_patche-1.0.1/tests/cases/efg +2 -0
- mseep_patche-1.0.1/tests/cases/lao +11 -0
- mseep_patche-1.0.1/tests/cases/tzu +13 -0
- mseep_patche-1.0.1/tests/test_apply.py +67 -0
- mseep_patche-1.0.1/tests/test_parse.py +1006 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 jingfelix
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: mseep-patche
|
3
|
+
Version: 1.0.1
|
4
|
+
Summary: Modern Patch in Python
|
5
|
+
Author-Email: mseep <support@skydeck.ai>
|
6
|
+
License: MIT
|
7
|
+
Requires-Python: >=3.10
|
8
|
+
Requires-Dist: typer[all]>=0.9.0
|
9
|
+
Requires-Dist: pydantic>=2.5.3
|
10
|
+
Requires-Dist: pydantic-settings>=2.2.1
|
11
|
+
Requires-Dist: whatthepatch-pydantic==1.0.6a3
|
12
|
+
Description-Content-Type: text/plain
|
13
|
+
|
14
|
+
Package managed by MseeP.ai
|
@@ -0,0 +1,45 @@
|
|
1
|
+
[project]
|
2
|
+
name = "mseep-patche"
|
3
|
+
version = "1.0.1"
|
4
|
+
description = "Modern Patch in Python"
|
5
|
+
authors = [
|
6
|
+
{ name = "mseep", email = "support@skydeck.ai" },
|
7
|
+
]
|
8
|
+
dependencies = [
|
9
|
+
"typer[all]>=0.9.0",
|
10
|
+
"pydantic>=2.5.3",
|
11
|
+
"pydantic-settings>=2.2.1",
|
12
|
+
"whatthepatch-pydantic==1.0.6a3",
|
13
|
+
]
|
14
|
+
requires-python = ">=3.10"
|
15
|
+
|
16
|
+
[project.readme]
|
17
|
+
content-type = "text/plain"
|
18
|
+
text = "Package managed by MseeP.ai"
|
19
|
+
|
20
|
+
[project.license]
|
21
|
+
text = "MIT"
|
22
|
+
|
23
|
+
[project.scripts]
|
24
|
+
patche = "Patche.__init__:app"
|
25
|
+
patche-mcp = "Patche.mcp.__init__:app"
|
26
|
+
|
27
|
+
[build-system]
|
28
|
+
requires = [
|
29
|
+
"pdm-backend",
|
30
|
+
]
|
31
|
+
build-backend = "pdm.backend"
|
32
|
+
|
33
|
+
[tool.pdm]
|
34
|
+
distribution = true
|
35
|
+
|
36
|
+
[tool.pdm.dev-dependencies]
|
37
|
+
dev = [
|
38
|
+
"viztracer>=0.16.3",
|
39
|
+
"whatthepatch>=1.0.7",
|
40
|
+
]
|
41
|
+
|
42
|
+
[dependency-groups]
|
43
|
+
mcp = [
|
44
|
+
"mcp[cli]>=1.6.0",
|
45
|
+
]
|
@@ -0,0 +1 @@
|
|
1
|
+
from .app import __version__, app
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.logging import RichHandler
|
6
|
+
|
7
|
+
logging.basicConfig(level=logging.INFO, handlers=[RichHandler()], format="%(message)s")
|
8
|
+
logger = logging.getLogger()
|
9
|
+
|
10
|
+
from Patche.__version__ import __version__
|
11
|
+
from Patche.utils.common import post_executed
|
12
|
+
|
13
|
+
app = typer.Typer(result_callback=post_executed, no_args_is_help=True)
|
14
|
+
|
15
|
+
|
16
|
+
@app.callback(invoke_without_command=True)
|
17
|
+
def callback(verbose: bool = False, version: bool = False):
|
18
|
+
"""
|
19
|
+
Entry for public options
|
20
|
+
"""
|
21
|
+
if verbose:
|
22
|
+
logger.setLevel(logging.DEBUG)
|
23
|
+
|
24
|
+
if version:
|
25
|
+
console = Console()
|
26
|
+
console.print(f"patche version {__version__}")
|
27
|
+
raise typer.Exit()
|
28
|
+
|
29
|
+
|
30
|
+
from Patche.commands.apply import apply
|
31
|
+
from Patche.commands.help import show_settings
|
32
|
+
from Patche.commands.show import show
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Annotated
|
3
|
+
|
4
|
+
import typer
|
5
|
+
|
6
|
+
from Patche.app import app, logger
|
7
|
+
from Patche.model import File
|
8
|
+
from Patche.utils.parse import parse_patch
|
9
|
+
from Patche.utils.resolve import apply_change
|
10
|
+
|
11
|
+
|
12
|
+
@app.command()
|
13
|
+
def apply(
|
14
|
+
patch_path: Annotated[str, typer.Argument(help="Path to the patch file")],
|
15
|
+
reverse: Annotated[
|
16
|
+
bool,
|
17
|
+
typer.Option(
|
18
|
+
"-R",
|
19
|
+
"--reverse",
|
20
|
+
help="Assume patches were created with old and new files swapped.",
|
21
|
+
),
|
22
|
+
] = False,
|
23
|
+
fuzz: Annotated[
|
24
|
+
int,
|
25
|
+
typer.Option(
|
26
|
+
"-F", "--fuzz", help="Set the fuzz factor to LINES for inexact matching."
|
27
|
+
),
|
28
|
+
] = 0,
|
29
|
+
):
|
30
|
+
"""
|
31
|
+
Apply a patch to a file.
|
32
|
+
"""
|
33
|
+
|
34
|
+
if not os.path.exists(patch_path):
|
35
|
+
logger.error(f"Warning: {patch_path} not found!")
|
36
|
+
raise typer.Exit(code=1)
|
37
|
+
|
38
|
+
if reverse:
|
39
|
+
logger.info("Reversing patch...")
|
40
|
+
|
41
|
+
has_failed = False
|
42
|
+
|
43
|
+
with open(patch_path, mode="r", encoding="utf-8") as (f):
|
44
|
+
diffes = parse_patch(f.read()).diff
|
45
|
+
|
46
|
+
for diff in diffes:
|
47
|
+
|
48
|
+
old_filename = diff.header.old_path
|
49
|
+
new_filename = diff.header.new_path
|
50
|
+
if reverse:
|
51
|
+
old_filename, new_filename = new_filename, old_filename
|
52
|
+
|
53
|
+
logger.debug(f"old_filename: {old_filename} new_filename: {new_filename}")
|
54
|
+
|
55
|
+
if old_filename == "/dev/null":
|
56
|
+
# 根据 diffes 创建新文件
|
57
|
+
try:
|
58
|
+
assert len(diff.hunks) == 1
|
59
|
+
|
60
|
+
new_line_list = []
|
61
|
+
for line in diff.changes:
|
62
|
+
|
63
|
+
assert line.old is None
|
64
|
+
new_line_list.append(line)
|
65
|
+
|
66
|
+
with open(new_filename, mode="w+", encoding="utf-8") as f:
|
67
|
+
for line in new_line_list:
|
68
|
+
f.write(line.content + "\n")
|
69
|
+
|
70
|
+
except AssertionError:
|
71
|
+
logger.error("Failed to create new file: invalid diff!")
|
72
|
+
raise typer.Exit(code=1)
|
73
|
+
|
74
|
+
elif new_filename == "/dev/null":
|
75
|
+
# 移除 old_filename
|
76
|
+
if os.path.exists(old_filename):
|
77
|
+
os.remove(old_filename)
|
78
|
+
else:
|
79
|
+
logger.error(f"{old_filename} not found!")
|
80
|
+
raise typer.Exit(code=1)
|
81
|
+
else:
|
82
|
+
if os.path.exists(old_filename):
|
83
|
+
|
84
|
+
logger.info(f"Applying patch to {old_filename}...")
|
85
|
+
|
86
|
+
new_line_list = File(file_path=old_filename).line_list
|
87
|
+
apply_result = apply_change(
|
88
|
+
diff.hunks, new_line_list, reverse=reverse, fuzz=fuzz
|
89
|
+
)
|
90
|
+
new_line_list = apply_result.new_line_list
|
91
|
+
|
92
|
+
# 检查失败数
|
93
|
+
for failed_hunk in apply_result.failed_hunk_list:
|
94
|
+
has_failed = True
|
95
|
+
logger.error(f"Failed hunk: {failed_hunk.index}")
|
96
|
+
else:
|
97
|
+
logger.error(f"{old_filename} not found!")
|
98
|
+
raise typer.Exit(code=1)
|
99
|
+
|
100
|
+
# 写入文件
|
101
|
+
if not has_failed:
|
102
|
+
with open(new_filename, mode="w+", encoding="utf-8") as f:
|
103
|
+
for line in new_line_list:
|
104
|
+
if line.status:
|
105
|
+
f.write(line.content + "\n")
|
106
|
+
|
107
|
+
raise typer.Exit(code=1 if has_failed else 0)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Annotated
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table
|
7
|
+
|
8
|
+
from Patche.app import app, logger
|
9
|
+
from Patche.utils.parse import parse_patch
|
10
|
+
|
11
|
+
|
12
|
+
@app.command()
|
13
|
+
def show(filename: Annotated[str, typer.Argument(help="Path to the patch file")]):
|
14
|
+
"""
|
15
|
+
Show details of a patch file.
|
16
|
+
"""
|
17
|
+
if not os.path.exists(filename):
|
18
|
+
logger.error(f"Warning: {filename} not found!")
|
19
|
+
return
|
20
|
+
|
21
|
+
content = ""
|
22
|
+
with open(filename, mode="r", encoding="utf-8") as (f):
|
23
|
+
content = f.read()
|
24
|
+
|
25
|
+
patch = parse_patch(content)
|
26
|
+
|
27
|
+
table = Table(box=None)
|
28
|
+
table.add_column("Field", justify="left", style="cyan")
|
29
|
+
table.add_column("Value", justify="left", style="magenta")
|
30
|
+
|
31
|
+
table.add_row("Patch", filename)
|
32
|
+
table.add_row("Sha", patch.sha)
|
33
|
+
table.add_row("Author", patch.author)
|
34
|
+
table.add_row("Date", (patch.date))
|
35
|
+
table.add_row("Subject", patch.subject)
|
36
|
+
|
37
|
+
for diff in patch.diff:
|
38
|
+
table.add_row("Diff", f"{diff.header.old_path} -> {diff.header.new_path}")
|
39
|
+
|
40
|
+
console = Console()
|
41
|
+
console.print(table)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import importlib.resources as pkg_resources
|
2
|
+
import os
|
3
|
+
from functools import lru_cache
|
4
|
+
|
5
|
+
from pydantic_settings import BaseSettings
|
6
|
+
|
7
|
+
|
8
|
+
@lru_cache()
|
9
|
+
def get_settings():
|
10
|
+
_settings = Settings()
|
11
|
+
if not os.path.exists(_settings.Config.env_file):
|
12
|
+
open(_settings.Config.env_file, "w").close()
|
13
|
+
|
14
|
+
return _settings
|
15
|
+
|
16
|
+
|
17
|
+
class Settings(BaseSettings):
|
18
|
+
max_diff_lines: int = 3
|
19
|
+
|
20
|
+
class Config:
|
21
|
+
env_file = os.path.join(os.environ.get("HOME"), ".Patche.env")
|
22
|
+
|
23
|
+
|
24
|
+
settings = get_settings()
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from typer import Typer
|
6
|
+
|
7
|
+
from Patche.app import logger
|
8
|
+
from Patche.mcp.server import serve
|
9
|
+
|
10
|
+
app = Typer()
|
11
|
+
|
12
|
+
|
13
|
+
@app.command()
|
14
|
+
# @app.option(
|
15
|
+
# "--repository",
|
16
|
+
# help="Repository to apply the patch",
|
17
|
+
# default=None,
|
18
|
+
# )
|
19
|
+
# @app.option(
|
20
|
+
# "--debug",
|
21
|
+
# help="Enable debug mode",
|
22
|
+
# default=False,
|
23
|
+
# )
|
24
|
+
def main(
|
25
|
+
repository: str = typer.Option(
|
26
|
+
None, "--repository", "-r", help="Repository to apply the patch"
|
27
|
+
),
|
28
|
+
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode"),
|
29
|
+
):
|
30
|
+
"""
|
31
|
+
Main entry point for the Patche MCP server.
|
32
|
+
"""
|
33
|
+
|
34
|
+
logger.info("Starting Patche MCP server...")
|
35
|
+
if debug:
|
36
|
+
logger.setLevel(logging.DEBUG)
|
37
|
+
|
38
|
+
asyncio.run(serve(repository))
|
39
|
+
|
40
|
+
|
41
|
+
if __name__ == "__main__":
|
42
|
+
main()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
# 定义 Patche 相关的数据模型
|
7
|
+
class PatcheConfig(BaseModel):
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
class PatcheList(BaseModel):
|
12
|
+
patche_dir: str
|
13
|
+
|
14
|
+
|
15
|
+
class PatcheShow(BaseModel):
|
16
|
+
patch_path: str
|
17
|
+
|
18
|
+
|
19
|
+
class PatcheApply(BaseModel):
|
20
|
+
patch_path: str
|
21
|
+
target_dir: str
|
22
|
+
reverse: bool = False
|
23
|
+
|
24
|
+
|
25
|
+
class PatcheTools(str, Enum):
|
26
|
+
CONFIG = "patche_config"
|
27
|
+
LIST = "patche_list"
|
28
|
+
SHOW = "patche_show"
|
29
|
+
APPLY = "patche_apply"
|