mirror of
https://github.com/kp2pml30/git-third-party.git
synced 2026-02-16 23:54:41 +04:00
fix: add logging, fix bugs
This commit is contained in:
parent
9b137600cd
commit
49cacfdb5e
3 changed files with 79 additions and 41 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/.ruff_cache
|
||||
__pycache__
|
||||
/third-party
|
||||
18
README.md
18
README.md
|
|
@ -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)
|
||||
- ...
|
||||
|
|
|
|||
|
|
@ -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', 'checkout', conf['commit']], check=True, cwd=target_dir)
|
||||
subprocess.run(
|
||||
['git', 'submodule', 'update', '--init', '--recursive', '--depth', '1'],
|
||||
[GIT, '-c', 'advice.detachedHead=false', 'checkout', conf['commit']],
|
||||
check=True,
|
||||
cwd=target_dir,
|
||||
)
|
||||
subprocess.run(
|
||||
[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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue