Skip to content

Commit 25a5ac1

Browse files
committed
init
Signed-off-by: ChaoLiu <[email protected]>
0 parents  commit 25a5ac1

File tree

17 files changed

+1950
-0
lines changed

17 files changed

+1950
-0
lines changed

.github/workflows/publish.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
- name: Set up Python
14+
uses: actions/setup-python@v4
15+
with:
16+
python-version: '3.x'
17+
18+
- name: Install dependencies
19+
run: |
20+
python -m pip install --upgrade pip
21+
pip install build twine
22+
23+
- name: Build and publish
24+
env:
25+
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
26+
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
27+
run: |
28+
python -m build
29+
twine upload dist/*

.github/workflows/tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11']
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install pytest pytest-cov
27+
pip install -e .
28+
29+
- name: Test with pytest
30+
run: |
31+
pytest --cov=splitpatch tests/

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
.coverage
9+
10+
# Virtual environments
11+
.venv

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 liuchao
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.

README.md

Lines changed: 328 additions & 0 deletions
Large diffs are not rendered by default.

README_zh-CN.md

Lines changed: 331 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[project]
2+
name = "splitpatch"
3+
version = "1.0"
4+
authors = [
5+
{ name="Chao Liu", email="[email protected]" },
6+
]
7+
description = "An intelligent tool for splitting large patch files into smaller file-based patches"
8+
readme = "README.md"
9+
requires-python = ">=3.8"
10+
classifiers = [
11+
"Programming Language :: Python :: 3",
12+
"Operating System :: OS Independent",
13+
]
14+
license = "MIT"
15+
license-files = ["LICEN[CS]E*"]
16+
17+
[project.urls]
18+
Homepage = "https://github.com/chaoliu719/splitpatch"
19+
Issues = "https://github.com/chaoliu719/splitpatch/issues"
20+
21+
[build-system]
22+
requires = ["setuptools >= 77.0.3"]
23+
build-backend = "setuptools.build_meta"
24+
25+
[tool.setuptools.packages.find]
26+
exclude = ["tests", "tests.*", ".github"]

splitpatch/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import logging
5+
6+
__version__ = "0.1.0"
7+
8+
logger = logging.getLogger(__name__)
9+
10+
if not logger.handlers:
11+
handler = logging.StreamHandler()
12+
formatter = logging.Formatter('[%(levelname)s] %(message)s')
13+
handler.setFormatter(formatter)
14+
logger.addHandler(handler)
15+
16+
def setup_logging(log_level: str = 'WARNING') -> None:
17+
"""Configure package-level logging settings
18+
19+
Args:
20+
log_level: Logging level, options: DEBUG, INFO, WARNING, ERROR, CRITICAL
21+
"""
22+
numeric_level = getattr(logging, log_level.upper(), None)
23+
if not isinstance(numeric_level, int):
24+
raise ValueError(f"Invalid log level: {log_level}")
25+
26+
logger.setLevel(numeric_level)
27+
28+
__all__ = ['__version__', 'logger', 'setup_logging']

splitpatch/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
from splitpatch.cli import main
5+
6+
if __name__ == '__main__':
7+
main()

splitpatch/cli.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import argparse
5+
import os
6+
import sys
7+
from typing import List
8+
9+
from splitpatch.patch import Patch
10+
from splitpatch.tree import DirNode
11+
from splitpatch.merge import Merge
12+
from splitpatch import __version__, logger, setup_logging
13+
14+
15+
def setup_args() -> argparse.Namespace:
16+
"""Set up and validate command line arguments
17+
18+
Includes:
19+
1. Parse command line arguments
20+
2. Validate argument validity
21+
3. Configure logging level
22+
4. Print argument information
23+
24+
Returns:
25+
argparse.Namespace: Parsed argument object
26+
"""
27+
parser = argparse.ArgumentParser(description='Split patch tool')
28+
29+
# Base parameters
30+
parser.add_argument('patch_files', type=str, nargs='+', help='Input patch file paths, multiple files can be specified')
31+
parser.add_argument('--out-dir', type=str, help='Output directory path')
32+
33+
# Split parameters
34+
parser.add_argument('--level', type=int, default=1, help='Merge level limit (default: 1)')
35+
parser.add_argument('--threshold', type=int, default=10,
36+
help='Module change file count threshold, merge to parent directory if below this value (default: 10)')
37+
38+
# Other parameters
39+
parser.add_argument('--dry-run', action='store_true', help='Only show operations to be performed, do not execute')
40+
parser.add_argument('--log-level', type=str, default='WARNING',
41+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
42+
help='Logging level (default: WARNING)')
43+
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
44+
45+
args = parser.parse_args()
46+
# Validate arguments
47+
if not args.dry_run and not args.out_dir:
48+
parser.error("--out-dir is required in non-dry-run mode")
49+
50+
# Configure logging
51+
setup_logging(args.log_level)
52+
logger.debug("Starting to process patch files")
53+
54+
# Print argument information
55+
logger.info("Current arguments:")
56+
logger.info(f" Input files: {', '.join(args.patch_files)}")
57+
if args.out_dir:
58+
logger.info(f" Output directory: {args.out_dir}")
59+
logger.info(f" Merge level: {args.level}")
60+
logger.info(f" File count threshold: {args.threshold}")
61+
logger.info(f" Log level: {args.log_level}")
62+
logger.info(f" Dry run: {'yes' if args.dry_run else 'no'}")
63+
64+
return args
65+
66+
67+
def parse_patches(patch_files: List[str]) -> Patch:
68+
"""Parse and validate all patch files
69+
70+
Args:
71+
patch_files: List of patch file paths
72+
73+
Returns:
74+
Patch: Combined patch object
75+
76+
Raises:
77+
SystemExit: When invalid patch files are found
78+
"""
79+
logger.debug("Starting to parse patch files")
80+
combined_patch = Patch("combined.patch")
81+
invalid_files = []
82+
83+
for patch_file in patch_files:
84+
patch = Patch(patch_file)
85+
if not patch.is_valid():
86+
invalid_files.append(patch_file)
87+
continue
88+
89+
logger.debug(f"Parsing patch file: {patch_file}")
90+
patch.parse_patch()
91+
92+
# Merge into combined data
93+
for file_path, changes in patch.items():
94+
if file_path in combined_patch:
95+
# If file exists, extend content
96+
combined_patch[file_path].extend(changes)
97+
else:
98+
combined_patch[file_path] = changes
99+
100+
if invalid_files:
101+
logger.error("The following patch files are invalid:")
102+
for file in invalid_files:
103+
logger.error(f" - {file}")
104+
sys.exit(1)
105+
106+
logger.debug(f"All patch files parsed: {combined_patch}")
107+
return combined_patch
108+
109+
def split_patch(patch: Patch, level: int, threshold: int) -> List[Patch]:
110+
"""Split and merge patch data based on level and threshold parameters
111+
112+
Args:
113+
patch: Patch object to be processed
114+
level: Level limit for merging
115+
threshold: File count threshold for module merging
116+
117+
Returns:
118+
List[Patch]: List of merged patches
119+
"""
120+
logger.debug("Processing patch data")
121+
122+
# Build file tree
123+
root = DirNode.from_patch(patch)
124+
logger.debug(f"Built file tree structure:\n{root}")
125+
126+
# Apply merge strategy
127+
strategy = Merge(root, level, threshold)
128+
strategy.merge()
129+
logger.debug(f"Merged file tree structure:\n{root}")
130+
131+
# Convert merged tree to patch list
132+
return root.to_patches()
133+
134+
135+
def output_patches(patches: List[Patch], out_dir: str, dry_run: bool) -> None:
136+
"""Output processed patches to specified directory
137+
138+
Args:
139+
patches: List of patches to output
140+
out_dir: Output directory path
141+
dry_run: If True, only print info without actually writing files
142+
"""
143+
if dry_run:
144+
logger.info("Dry run mode - files will not be written")
145+
for i, patch in enumerate(patches, 1):
146+
normalized_path = patch.path.lstrip('/').replace('/', '_')
147+
logger.info(f"Patch {i:03d}_{normalized_path}.patch")
148+
return
149+
150+
os.makedirs(out_dir, exist_ok=True)
151+
for i, patch in enumerate(patches, 1):
152+
normalized_path = patch.path.lstrip('/').replace('/', '_')
153+
output_file = os.path.join(out_dir, f"{i:03d}_{normalized_path}.patch")
154+
155+
try:
156+
patch.path = output_file
157+
patch.write_patch()
158+
logger.info(f"Output file: {patch}")
159+
except IOError as e:
160+
logger.error(f"Failed to write file {output_file}: {e}")
161+
sys.exit(1)
162+
163+
164+
def main() -> None:
165+
try:
166+
# Parse command line arguments
167+
args = setup_args()
168+
169+
# Parse patch files
170+
combined_patch = parse_patches(args.patch_files)
171+
172+
# Process patch data
173+
patches = split_patch(combined_patch, args.level, args.threshold)
174+
175+
# Output results
176+
output_patches(patches, args.out_dir, args.dry_run)
177+
178+
except Exception as e:
179+
logger.error(f"An error occurred during processing: {e}")
180+
sys.exit(1)
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)