fix: add logging, fix bugs

This commit is contained in:
kp2pml30 2025-03-22 03:19:05 +04:00
parent 9b137600cd
commit 49cacfdb5e
3 changed files with 79 additions and 41 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.ruff_cache
__pycache__
/third-party

View file

@ -1,10 +1,10 @@
# Git third party
Small zero-dependency python utility that is an alternative to [git-submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) and [git-subtree](https://manpages.debian.org/testing/git-man/git-subtree.1.en.html), that allows patching third-party code, storing commits in your tree
Tool for patching third-party libraries without need to fork them
No need to fork, no need to store all code in your tree, store just your patches
This is a small zero-dependency utility that is an alternative to [git-submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) and [git-subtree](https://manpages.debian.org/testing/git-man/git-subtree.1.en.html). It stores only delta (changes) inside your tree
**WARNING:** it is a raw prototype
This tool is not oriented for highly concurrent modification of third-party tools by multiple users
## Usage
```bash
@ -25,9 +25,11 @@ git-third-party update third-party/RustPython
## How it works?
It stores (at `/.gitthirdparty`) a configutation that describes all third party repositories and patches (at `/.gitthirdparty-patches/<local/repo/path>`). `save` command updates the patches, `update` command reapplies them
It stores (at `/.git-third-party/`) a configuration that describes all third party repositories and patches. `save` command updates the patches, `update` command reapplies them
## Todo
- [ ] commit hooks
- [ ] updating all
- [ ] automated tests
Directory `.git-third-party` must be controlled by git, while third-party repos should not
Best effort is done to keep patches deterministic and strip all metadata from them, which includes:
- git version
- commit hash (which depends on committer)
- ...

View file

@ -2,7 +2,7 @@
#
# This file is part of the git-third-party distribution (https://github.com/kp2pml30/git-third-party).
# Copyright (c) 2024 Kira Prokopenko kp2pml30@gmail.com
# Copyright (c) 2024-2025 Kira Prokopenko kp2pml30@gmail.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -24,31 +24,43 @@ import os
import sys
import abc
import typing
import json
import textwrap
import io
import logging
from pathlib import Path
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
GIT = os.getenv('GIT', 'git')
try:
top_dir_str = subprocess.check_output(
['git', 'rev-parse', '--show-toplevel'], text=True
[GIT, 'rev-parse', '--show-toplevel'], text=True
)
except Exception as e:
print(
f'could not execute `{GIT} rev-parse --show-toplevel`. You may wish to set `GIT` env variable',
file=sys.stderr,
)
except:
exit(1)
top_dir = Path(top_dir_str.strip())
cur_dir = Path(os.getcwd())
config_path = top_dir.joinpath('.gitthirdparty')
if not config_path.exists():
SAVED_FILES_PATH = top_dir.joinpath('.git-third-party')
PATCHES_PATH = SAVED_FILES_PATH.joinpath('patches')
CONFIG_PATH = SAVED_FILES_PATH.joinpath('config.json')
if not PATCHES_PATH.exists():
logging.info(f'patches directory does not exist, creating path=`{PATCHES_PATH}`')
PATCHES_PATH.mkdir(parents=True, exist_ok=True)
if not CONFIG_PATH.exists():
config = {'repos': {}}
else:
import json
with open(config_path) as f:
config = json.load(f)
import textwrap
import io
config = json.loads(CONFIG_PATH.read_text())
class Command:
@ -59,19 +71,20 @@ class Command:
def handle(args: list[str]) -> None: ...
def show_help(self, file: io.StringIO):
file.write(f'subcommand `{self.name}`:')
file.write(textwrap.indent(textwrap.dedent(self.help), prefix='\t'))
file.write(f'subcommand `{self.name}`:\n')
file.write(textwrap.indent(textwrap.dedent(self.help), prefix='\t').rstrip())
file.write('\n')
def _get_patches_dir(name: str) -> Path:
return top_dir.joinpath('.gitthirdparty-patches', *name.split('/'))
return PATCHES_PATH.joinpath(*name.split('/'))
def _dirty_check(name: str) -> None:
target_dir = top_dir.joinpath(*name.split('/'))
for opts in ([], ['--cached']):
rs = subprocess.run(
['git', 'diff', '--exit-code', '--quiet', *opts], check=False, cwd=target_dir
[GIT, 'diff', '--exit-code', '--quiet', *opts], check=False, cwd=target_dir
)
if rs.returncode != 0:
raise GitThirdPartyException(f'repo {name} is dirty ( {target_dir} )')
@ -80,37 +93,49 @@ def _dirty_check(name: str) -> None:
def _update_at(name: str) -> None:
conf = config['repos'][name]
target_dir = top_dir.joinpath(*name.split('/'))
logging.info(f'updating name={name} at path={target_dir}')
if not target_dir.exists():
target_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(['git', 'init'], check=True, cwd=target_dir)
subprocess.run([GIT, 'init'], check=True, cwd=target_dir)
subprocess.run(
['git', 'remote', 'add', 'origin', conf['url']], check=True, cwd=target_dir
[GIT, 'remote', 'add', 'origin', conf['url']], check=True, cwd=target_dir
)
_dirty_check(name)
rs = subprocess.run(['git', 'checkout', conf['commit']], check=False, cwd=target_dir)
rs = subprocess.run(
[GIT, '-c', 'advice.detachedHead=false', 'checkout', conf['commit']],
check=False,
cwd=target_dir,
)
if rs.returncode != 0:
logging.info(f'checking out {conf["commit"]} failed, fetching')
subprocess.run(
['git', 'fetch', 'origin', '--depth=1', conf['commit']],
[GIT, 'fetch', 'origin', '--depth=1', conf['commit']],
check=True,
cwd=target_dir,
)
subprocess.run(
[GIT, '-c', 'advice.detachedHead=false', 'checkout', conf['commit']],
check=True,
cwd=target_dir,
)
subprocess.run(['git', 'checkout', conf['commit']], check=True, cwd=target_dir)
subprocess.run(
['git', 'submodule', 'update', '--init', '--recursive', '--depth', '1'],
[GIT, 'submodule', 'update', '--init', '--recursive', '--depth', '1'],
check=True,
cwd=target_dir,
)
submodules = conf.get('submodules', None)
if submodules is None:
logging.info('updating all submodules')
subprocess.run(
['git', 'submodule', 'update', '--recursive', '--depth', '1'],
[GIT, 'submodule', 'update', '--recursive', '--depth', '1'],
check=True,
cwd=target_dir,
)
elif len(submodules) != 0:
logging.info(f'updating submodules {submodules}')
subprocess.run(
['git', 'submodule', 'update', '--recursive', '--depth', '1', '--'] + submodules,
[GIT, 'submodule', 'update', '--recursive', '--depth', '1', '--'] + submodules,
check=True,
cwd=target_dir,
)
@ -118,7 +143,9 @@ def _update_at(name: str) -> None:
patches_dir = _get_patches_dir(name)
patch_files = [patches_dir.joinpath(str(i)) for i in range(1, conf['patches'] + 1)]
if len(patch_files) != 0:
subprocess.run(['git', 'am', *patch_files], cwd=target_dir, check=True)
subprocess.run([GIT, 'am', *patch_files], cwd=target_dir, check=True)
else:
logging.warning(f'no patches detected')
class GitThirdPartyException(Exception):
@ -139,6 +166,8 @@ class AddCommand(Command):
help = f"""\
{EXE_NAME} {name} <PATH> <REPO URL> <COMMIT>
Adds a new repository to track
Example: `{EXE_NAME} {name} third-party/my-cool-tool https://github.com/kp2pml30/git-third-party 4e6cc662a7e2b409e36d2412440c5670d6034431`
"""
def handle(self, args: list[str]) -> None:
@ -157,7 +186,7 @@ class AddCommand(Command):
print(f'repo ({name}) is already in config', file=sys.stderr)
exit(1)
target_dir = top_dir.joinpath(*name.split('/'))
res = subprocess.run(['git', 'check-ignore', target_dir], check=False)
res = subprocess.run([GIT, 'check-ignore', target_dir], check=False)
if res.returncode != 0:
raise GitThirdPartyException(
f'target directory ({target_dir}) is not ignored by git'
@ -191,7 +220,7 @@ class UpdateCommand(Command):
restore state of specified repositories
"""
def handle(args):
def handle(self, args):
names = _get_names_from_paths(args)
for name in names:
_update_at(name)
@ -206,7 +235,7 @@ class SaveCommand(Command):
record state of specified repositories
"""
def handle(args):
def handle(self, args):
names = _get_names_from_paths(args)
for name in names:
_save_at(name)
@ -214,15 +243,17 @@ class SaveCommand(Command):
def _save_at(name: str) -> None:
_dirty_check(name)
target_dir = top_dir.joinpath(*name.split('/'))
logging.info(f'saving name={name} at path={target_dir}')
conf = config['repos'][name]
patches_dir = _get_patches_dir(name)
for i in range(1, conf['patches'] + 1):
patches_dir.joinpath(str(i)).unlink()
patches_dir.mkdir(parents=True, exist_ok=True)
target_dir = top_dir.joinpath(*name.split('/'))
subprocess.run(
[
'git',
GIT,
'format-patch',
f'{conf["commit"]}..HEAD',
'--no-numbered',
@ -236,13 +267,15 @@ def _save_at(name: str) -> None:
cwd=target_dir,
)
new_patches = subprocess.run(
['git', 'rev-list', '--count', f'{conf["commit"]}..HEAD'],
[GIT, 'rev-list', '--count', f'{conf["commit"]}..HEAD'],
check=True,
cwd=target_dir,
text=True,
capture_output=True,
)
conf['patches'] = int(new_patches.stdout.strip())
patches_cnt = int(new_patches.stdout.strip())
logging.info(f'number of patches is {patches_cnt}')
conf['patches'] = patches_cnt
mode = sys.argv[1] if len(sys.argv) > 1 else 'help'
@ -270,6 +303,6 @@ except GitThirdPartyException as e:
import json
with open(config_path, 'wt') as f:
with open(CONFIG_PATH, 'wt') as f:
json.dump(config, f, indent='\t')
f.write('\n')