git-nested 1.2.0__py3-none-any.whl → 1.2.2__py3-none-any.whl
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.
- git_nested/checks.py +21 -8
- git_nested/cli/location.py +17 -1
- git_nested/cli/parser.py +69 -2
- git_nested/cli/setup.py +8 -3
- git_nested/cli/spec.py +190 -4
- git_nested/commands/__init__.py +6 -0
- git_nested/commands/branch.py +34 -2
- git_nested/commands/clean.py +1 -1
- git_nested/commands/clone.py +53 -15
- git_nested/commands/commit.py +8 -4
- git_nested/commands/config.py +5 -4
- git_nested/commands/diff.py +3 -1
- git_nested/commands/init.py +7 -4
- git_nested/commands/patchset.py +222 -0
- git_nested/commands/pull.py +132 -9
- git_nested/commands/push.py +55 -5
- git_nested/commands/status.py +4 -2
- git_nested/commands/submodules.py +885 -0
- git_nested/commands/verify.py +153 -0
- git_nested/configio.py +91 -0
- git_nested/content.py +147 -5
- git_nested/discovery.py +1 -1
- git_nested/filters.py +48 -0
- git_nested/git.py +19 -2
- git_nested/gitfile.py +23 -8
- git_nested/history.py +83 -12
- git_nested/messages.py +9 -7
- git_nested/models.py +63 -4
- git_nested/patchbuild.py +128 -0
- git_nested/patches.py +300 -0
- git_nested/patchpick.py +55 -0
- git_nested/refs.py +11 -6
- git_nested/submodules.py +29 -7
- git_nested/worktree.py +15 -0
- {git_nested-1.2.0.dist-info → git_nested-1.2.2.dist-info}/METADATA +13 -2
- git_nested-1.2.2.dist-info/RECORD +58 -0
- {git_nested-1.2.0.dist-info → git_nested-1.2.2.dist-info}/WHEEL +1 -1
- git_nested/yamlio.py +0 -25
- git_nested-1.2.0.dist-info/RECORD +0 -52
- {git_nested-1.2.0.dist-info → git_nested-1.2.2.dist-info}/entry_points.txt +0 -0
- {git_nested-1.2.0.dist-info → git_nested-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {git_nested-1.2.0.dist-info → git_nested-1.2.2.dist-info}/top_level.txt +0 -0
git_nested/checks.py
CHANGED
|
@@ -20,7 +20,7 @@ def check_repository(git: GitRunner, command: str, flags: Flags) -> tuple[Path |
|
|
|
20
20
|
return None, None
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
git.
|
|
23
|
+
git_dir, inside_work_tree = git.check_output(['rev-parse', '--git-dir', '--is-inside-work-tree']).splitlines()
|
|
24
24
|
except GitNestedError:
|
|
25
25
|
# A deliberate re-interpretation of git's own message, not an
|
|
26
26
|
# incidental failure, so the chain is suppressed -- and since
|
|
@@ -29,14 +29,14 @@ def check_repository(git: GitRunner, command: str, flags: Flags) -> tuple[Path |
|
|
|
29
29
|
raise GitNestedError("not inside a git repository") from None
|
|
30
30
|
|
|
31
31
|
# Per worktree of this repository, so they do not block each other.
|
|
32
|
-
git_tmp = Path(
|
|
32
|
+
git_tmp = Path(git_dir) / 'tmp'
|
|
33
33
|
|
|
34
34
|
# Not a nested branch. A detached HEAD is fine: commits go there, as with git commit.
|
|
35
35
|
current_branch = git.check_output(['symbolic-ref', '--short', '--quiet', 'HEAD'], may_fail=True)
|
|
36
36
|
if current_branch.startswith('nested/'):
|
|
37
37
|
raise GitNestedError(f"can't {command} while the nested branch {current_branch} is checked out")
|
|
38
38
|
|
|
39
|
-
if
|
|
39
|
+
if inside_work_tree != 'true':
|
|
40
40
|
raise GitNestedError("not inside a git working tree")
|
|
41
41
|
|
|
42
42
|
if _needs_clean_worktree(command, flags):
|
|
@@ -55,7 +55,18 @@ def _needs_clean_worktree(command: str, flags: Flags) -> bool:
|
|
|
55
55
|
|
|
56
56
|
def check_worktree_clean(git: GitRunner, command: str) -> None:
|
|
57
57
|
"""Ensure working copy has no uncommitted changes."""
|
|
58
|
-
if command not in [
|
|
58
|
+
if command not in [
|
|
59
|
+
'clone',
|
|
60
|
+
'init',
|
|
61
|
+
'pull',
|
|
62
|
+
'push',
|
|
63
|
+
'branch',
|
|
64
|
+
'commit',
|
|
65
|
+
'diff',
|
|
66
|
+
'repair',
|
|
67
|
+
'drop a patch',
|
|
68
|
+
'cherry-pick a patch',
|
|
69
|
+
]:
|
|
59
70
|
return
|
|
60
71
|
|
|
61
72
|
pwd = Path.cwd().as_posix()
|
|
@@ -74,13 +85,15 @@ def check_worktree_clean(git: GitRunner, command: str) -> None:
|
|
|
74
85
|
raise GitNestedError(f"{pwd}: can't {command}, the index has changes")
|
|
75
86
|
|
|
76
87
|
|
|
77
|
-
def warn_uncommitted_subdir_changes(
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
def warn_uncommitted_subdir_changes(
|
|
89
|
+
git: GitRunner, command: str, flags: Flags, subdir: Path, skip: str | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Warn when `command` ignores uncommitted changes in `subdir`, but not in `skip` inside it."""
|
|
92
|
+
if command not in ['branch', 'diff', 'patches', 'push', 'verify'] or (command == 'push' and flags.commit):
|
|
80
93
|
return
|
|
81
94
|
|
|
82
95
|
git.run(['update-index', '-q', '--ignore-submodules', '--refresh'], may_fail=True)
|
|
83
|
-
pathspec = ['--', subdir.as_posix()]
|
|
96
|
+
pathspec = ['--', subdir.as_posix(), *([f':(exclude,literal){subdir.as_posix()}/{skip}'] if skip else [])]
|
|
84
97
|
if git.run(['diff-files', '--quiet', '--ignore-submodules', *pathspec], may_fail=True).returncode != 0:
|
|
85
98
|
output.warn(f"{subdir.as_posix()}: has uncommitted changes; {command} uses only committed content")
|
|
86
99
|
return
|
git_nested/cli/location.py
CHANGED
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
# Commands whose missing <subdir> is not filled in from the current directory:
|
|
30
30
|
# status reports on all of them, clone guesses one from the upstream, and init
|
|
31
31
|
# is what makes a directory nested in the first place.
|
|
32
|
-
_NO_IMPLIED_SUBDIR = ('clone', 'completion', 'init', 'status', 'version')
|
|
32
|
+
_NO_IMPLIED_SUBDIR = ('clone', 'completion', 'init', 'status', 'submodules', 'version')
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def at_top_level(git: GitRunner, command: str, ctx: CommandContext) -> AbstractContextManager[object]:
|
|
@@ -49,14 +49,20 @@ def at_top_level(git: GitRunner, command: str, ctx: CommandContext) -> AbstractC
|
|
|
49
49
|
# Path options are made absolute, so moving to the top level does not change what they name.
|
|
50
50
|
ctx.flags.dir = _absolute(ctx.flags.dir)
|
|
51
51
|
ctx.flags.message_file = _absolute(ctx.flags.message_file)
|
|
52
|
+
ctx.flags.output_dir = _absolute(ctx.flags.output_dir)
|
|
53
|
+
ctx.patches = _all_absolute(ctx.patches)
|
|
52
54
|
# Remotes given as relative paths are rebased onto the top level instead.
|
|
53
55
|
# Relative rather than absolute: a remote ends up in .gitnested, and a
|
|
54
56
|
# relative one keeps working in every clone of the parent repository.
|
|
55
57
|
ctx.upstream = _remote_from(prefix, ctx.upstream)
|
|
56
58
|
ctx.flags.remote = _remote_from(prefix, ctx.flags.remote)
|
|
59
|
+
ctx.flags.resolve_relative = _remote_from(prefix, ctx.flags.resolve_relative)
|
|
57
60
|
if ctx.config_key == 'remote':
|
|
58
61
|
ctx.config_value = _remote_from(prefix, ctx.config_value)
|
|
59
62
|
|
|
63
|
+
# --ignore and --only paths are given like <subdir>.
|
|
64
|
+
ctx.flags.ignore = _all_joined(prefix, ctx.flags.ignore)
|
|
65
|
+
ctx.flags.only = _all_joined(prefix, ctx.flags.only)
|
|
60
66
|
if ctx.subdir is not None:
|
|
61
67
|
ctx.subdir = _joined(prefix, Path(ctx.subdir))
|
|
62
68
|
elif command not in _NO_IMPLIED_SUBDIR and not ctx.flags.all:
|
|
@@ -73,6 +79,11 @@ def _absolute(path: str | None) -> str | None:
|
|
|
73
79
|
return os.path.abspath(path) if path else None # noqa: PTH100
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
def _all_absolute(paths: list[str] | None) -> list[str] | None:
|
|
83
|
+
"""Each of `paths` made absolute."""
|
|
84
|
+
return [os.path.abspath(path) for path in paths] if paths else paths # noqa: PTH100
|
|
85
|
+
|
|
86
|
+
|
|
76
87
|
def _remote_from(prefix: str, remote: str | None) -> str | None:
|
|
77
88
|
"""`remote`, typed below the top level at `prefix`, as seen from the top level.
|
|
78
89
|
|
|
@@ -90,6 +101,11 @@ def _remote_from(prefix: str, remote: str | None) -> str | None:
|
|
|
90
101
|
return joined if joined.startswith('.') else f'./{joined}'
|
|
91
102
|
|
|
92
103
|
|
|
104
|
+
def _all_joined(prefix: str, paths: list[str] | None) -> list[str] | None:
|
|
105
|
+
"""Each of `paths` joined like a <subdir>. None stays None."""
|
|
106
|
+
return [_joined(prefix, Path(path)) for path in paths] if paths else paths
|
|
107
|
+
|
|
108
|
+
|
|
93
109
|
def _joined(prefix: str, subdir: Path) -> str:
|
|
94
110
|
"""`subdir`, typed below the top level at `prefix`, as a path from the top level."""
|
|
95
111
|
if subdir.anchor:
|
git_nested/cli/parser.py
CHANGED
|
@@ -69,7 +69,13 @@ def _flags_from_args(args: argparse.Namespace) -> Flags:
|
|
|
69
69
|
dry_run=getattr(args, 'dry_run', False),
|
|
70
70
|
filter=getattr(args, 'filter', []),
|
|
71
71
|
force=getattr(args, 'force', False),
|
|
72
|
+
ignore=getattr(args, 'ignore', None),
|
|
73
|
+
only=getattr(args, 'only', None),
|
|
74
|
+
resolve_relative=getattr(args, 'resolve_relative', None),
|
|
75
|
+
recursive=getattr(args, 'recursive', False),
|
|
72
76
|
from_submodule=getattr(args, 'from_submodule', False),
|
|
77
|
+
from_patches=getattr(args, 'from_patches', False),
|
|
78
|
+
local=getattr(args, 'local', False),
|
|
73
79
|
recurse_submodules=getattr(args, 'recurse_submodules', False),
|
|
74
80
|
gpg_sign=getattr(args, 'gpg_sign', False),
|
|
75
81
|
signoff=getattr(args, 'signoff', False),
|
|
@@ -79,6 +85,9 @@ def _flags_from_args(args: argparse.Namespace) -> Flags:
|
|
|
79
85
|
no_squash=getattr(args, 'no_squash', False),
|
|
80
86
|
squash=getattr(args, 'squash', False),
|
|
81
87
|
update=getattr(args, 'update', False),
|
|
88
|
+
keep=getattr(args, 'keep', False),
|
|
89
|
+
resume=getattr(args, 'resume', False),
|
|
90
|
+
abort=getattr(args, 'abort', False),
|
|
82
91
|
quiet=getattr(args, 'quiet', False),
|
|
83
92
|
# argparse's `count` action leaves the attribute at None when the flag
|
|
84
93
|
# is absent, and the output layer wants a real count.
|
|
@@ -102,6 +111,7 @@ _EXCLUSIVE_FLAGS = (
|
|
|
102
111
|
('all', 'dir', "--dir names one worktree, so it can't be used with --all"),
|
|
103
112
|
('all', 'name', "--name names one branch, so it can't be used with --all"),
|
|
104
113
|
('dry_run', 'commit', "--dry-run can't be combined with --commit"),
|
|
114
|
+
('from_patches', 'gpg_sign', "--from-patches can't sign the commits, so it can't be used with -S"),
|
|
105
115
|
)
|
|
106
116
|
|
|
107
117
|
|
|
@@ -114,6 +124,56 @@ def _check_flags(flags: Flags) -> None:
|
|
|
114
124
|
output.usage_error("--update needs --branch or --remote to have something to update")
|
|
115
125
|
|
|
116
126
|
|
|
127
|
+
def _verify_files(args: argparse.Namespace) -> tuple[list[str] | None, str]:
|
|
128
|
+
"""The files of --patches or --diffs, and which of the two it is."""
|
|
129
|
+
patches, diffs = getattr(args, 'patches', None), getattr(args, 'diffs', None)
|
|
130
|
+
if patches is not None and diffs is not None:
|
|
131
|
+
output.usage_error("--patches and --diffs can't be used together")
|
|
132
|
+
if diffs is not None:
|
|
133
|
+
return diffs, 'diff'
|
|
134
|
+
return patches, 'patches'
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _options_last(args_list: list[str], command: str) -> list[str]:
|
|
138
|
+
"""For `command`, its options after its positionals: `patches cherry-pick -r X bar c` as `... bar c -r X`.
|
|
139
|
+
|
|
140
|
+
Before Python 3.12, argparse can't take a positional after an option
|
|
141
|
+
once an earlier positional is parsed. Moving the options to the end
|
|
142
|
+
keeps each option with its value.
|
|
143
|
+
"""
|
|
144
|
+
start = next((index for index, arg in enumerate(args_list) if not arg.startswith('-')), None)
|
|
145
|
+
if start is None or args_list[start] != command:
|
|
146
|
+
return args_list
|
|
147
|
+
positionals, options = _split_options(args_list[start + 1 :], _taking_a_value(command))
|
|
148
|
+
return [*args_list[: start + 1], *positionals, *options]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _taking_a_value(command: str) -> set[str]:
|
|
152
|
+
"""The option names of `command` that take a value."""
|
|
153
|
+
return {
|
|
154
|
+
name
|
|
155
|
+
for opt, names, kwargs in SUBPARSER_ARG_SPECS
|
|
156
|
+
if opt in VALID_COMMAND_OPTIONS[command] and kwargs.get('action') not in ('store_true', 'store_const')
|
|
157
|
+
for name in names
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _split_options(args: list[str], takes_value: set[str]) -> tuple[list[str], list[str]]:
|
|
162
|
+
"""`args` as positionals and options, each option with its value."""
|
|
163
|
+
positionals: list[str] = []
|
|
164
|
+
options: list[str] = []
|
|
165
|
+
rest = iter(args)
|
|
166
|
+
for arg in rest:
|
|
167
|
+
if not arg.startswith('-'):
|
|
168
|
+
positionals.append(arg)
|
|
169
|
+
continue
|
|
170
|
+
options.append(arg)
|
|
171
|
+
value = next(rest, None) if arg in takes_value else None
|
|
172
|
+
if value is not None:
|
|
173
|
+
options.append(value)
|
|
174
|
+
return positionals, options
|
|
175
|
+
|
|
176
|
+
|
|
117
177
|
def parse_args(git: GitRunner, args_list: list[str]) -> tuple[str, CommandContext]:
|
|
118
178
|
"""Parse command line arguments into a command name and the context to run it with.
|
|
119
179
|
|
|
@@ -121,7 +181,7 @@ def parse_args(git: GitRunner, args_list: list[str]) -> tuple[str, CommandContex
|
|
|
121
181
|
tuple: (command, context). The context's git_tmp/head_commit are left
|
|
122
182
|
unset -- only the caller knows whether the command runs in a repository.
|
|
123
183
|
"""
|
|
124
|
-
args = build_arg_parser().parse_args(args_list)
|
|
184
|
+
args = build_arg_parser().parse_args(_options_last(_options_last(args_list, 'patches'), 'submodules'))
|
|
125
185
|
|
|
126
186
|
if args.version:
|
|
127
187
|
args.command = 'version'
|
|
@@ -130,9 +190,10 @@ def parse_args(git: GitRunner, args_list: list[str]) -> tuple[str, CommandContex
|
|
|
130
190
|
|
|
131
191
|
flags = _flags_from_args(args)
|
|
132
192
|
_check_flags(flags)
|
|
133
|
-
if flags.single_commit and not flags.all:
|
|
193
|
+
if flags.single_commit and not flags.all and args.command != 'submodules':
|
|
134
194
|
output.usage_error("--single-commit needs --all")
|
|
135
195
|
|
|
196
|
+
patches, verify_mode = _verify_files(args)
|
|
136
197
|
upstream = getattr(args, 'upstream', None)
|
|
137
198
|
subdir = getattr(args, 'subdir', None)
|
|
138
199
|
if upstream and not subdir:
|
|
@@ -150,5 +211,11 @@ def parse_args(git: GitRunner, args_list: list[str]) -> tuple[str, CommandContex
|
|
|
150
211
|
config_key=getattr(args, 'key', None),
|
|
151
212
|
config_value=getattr(args, 'value', None),
|
|
152
213
|
git_args=getattr(args, 'git_args', None),
|
|
214
|
+
patches_action=getattr(args, 'action', None),
|
|
215
|
+
patch_names=getattr(args, 'patch_names', None),
|
|
216
|
+
patches=patches,
|
|
217
|
+
verify_mode=verify_mode,
|
|
218
|
+
submodules_action=getattr(args, 'submodules_action', None),
|
|
219
|
+
submodules_args=getattr(args, 'submodules_args', None),
|
|
153
220
|
)
|
|
154
221
|
return args.command, context
|
git_nested/cli/setup.py
CHANGED
|
@@ -48,7 +48,7 @@ def _check_existing_worktree(git: GitRunner, command: str, subdir: Path, gitnest
|
|
|
48
48
|
output.error(
|
|
49
49
|
f"{subdir.as_posix()}: no worktree to commit from; run 'git nested branch {subdir.as_posix()}' first"
|
|
50
50
|
)
|
|
51
|
-
elif command not in ['branch', 'clean', 'commit', 'push', 'run'] and worktree_path is not None:
|
|
51
|
+
elif command not in ['branch', 'clean', 'commit', 'patches', 'push', 'run', 'verify'] and worktree_path is not None:
|
|
52
52
|
output.error(messages.worktree_exists(subdir, worktree_path, prunable=gitnested.exists()))
|
|
53
53
|
|
|
54
54
|
|
|
@@ -61,7 +61,7 @@ def setup_command(
|
|
|
61
61
|
tuple: (subdir, gitnested, subref, config)
|
|
62
62
|
"""
|
|
63
63
|
subdir = resolve_subdir(subdir)
|
|
64
|
-
subref = refs.sanitize_subref(
|
|
64
|
+
subref = refs.sanitize_subref(subdir.as_posix())
|
|
65
65
|
gitnested = resolve_gitnested_file(subdir)
|
|
66
66
|
|
|
67
67
|
if not flags.force:
|
|
@@ -69,9 +69,14 @@ def setup_command(
|
|
|
69
69
|
|
|
70
70
|
# clone and init have no .gitnested yet. clean does not need it.
|
|
71
71
|
if command in ['clone', 'init', 'clean']:
|
|
72
|
+
# Check nested.format now, not after the content is in place.
|
|
73
|
+
if command != 'clean':
|
|
74
|
+
gitfile.new_file_is_toml(git)
|
|
72
75
|
config = NestedConfig(remote=flags.remote or upstream or '', branch=flags.branch or '', pin=flags.pin or '')
|
|
73
76
|
else:
|
|
74
77
|
config = gitfile.read_config(gitnested, flags)
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
# `patches` reads the patches folder as it is, committed or not
|
|
80
|
+
skip = config.patches_inside(subdir) if command == 'patches' else None
|
|
81
|
+
checks.warn_uncommitted_subdir_changes(git, command, flags, subdir, skip)
|
|
77
82
|
return subdir, gitnested, subref, config
|
git_nested/cli/spec.py
CHANGED
|
@@ -13,7 +13,7 @@ from ..constants import COMPLETION_SHELLS
|
|
|
13
13
|
# Which flags each command accepts. The keys are also the command list:
|
|
14
14
|
# the subparsers, and the completion candidates, are built from them.
|
|
15
15
|
VALID_COMMAND_OPTIONS: dict[str, list[str]] = {
|
|
16
|
-
'branch': ['all', 'dir', 'fetch', 'force', 'gpg_sign', 'name'],
|
|
16
|
+
'branch': ['all', 'dir', 'fetch', 'force', 'from_patches', 'gpg_sign', 'name'],
|
|
17
17
|
'clean': ['ALL', 'all', 'force'],
|
|
18
18
|
'clone': [
|
|
19
19
|
'adopt',
|
|
@@ -31,17 +31,19 @@ VALID_COMMAND_OPTIONS: dict[str, list[str]] = {
|
|
|
31
31
|
'recurse_submodules',
|
|
32
32
|
'signoff',
|
|
33
33
|
],
|
|
34
|
-
'commit': ['fetch', 'force', 'gpg_sign', 'message', 'msg_file', 'signoff'],
|
|
34
|
+
'commit': ['fetch', 'force', 'gpg_sign', 'message', 'msg_file', 'pin', 'signoff'],
|
|
35
35
|
'completion': [],
|
|
36
36
|
'config': [],
|
|
37
37
|
'diff': ['all', 'branch', 'remote'],
|
|
38
38
|
'fetch': ['all', 'branch', 'force', 'remote'],
|
|
39
39
|
'init': ['branch', 'from_submodule', 'gpg_sign', 'remote', 'method', 'signoff', 'upstream_commit'],
|
|
40
|
+
'patches': ['all', 'local', 'remote'],
|
|
40
41
|
'pull': [
|
|
41
42
|
'all',
|
|
42
43
|
'branch',
|
|
43
44
|
'force',
|
|
44
45
|
'gpg_sign',
|
|
46
|
+
'ignore',
|
|
45
47
|
'merge',
|
|
46
48
|
'message',
|
|
47
49
|
'method',
|
|
@@ -63,6 +65,7 @@ VALID_COMMAND_OPTIONS: dict[str, list[str]] = {
|
|
|
63
65
|
'dry_run',
|
|
64
66
|
'force',
|
|
65
67
|
'gpg_sign',
|
|
68
|
+
'ignore',
|
|
66
69
|
'merge',
|
|
67
70
|
'message',
|
|
68
71
|
'method',
|
|
@@ -76,6 +79,19 @@ VALID_COMMAND_OPTIONS: dict[str, list[str]] = {
|
|
|
76
79
|
'update',
|
|
77
80
|
],
|
|
78
81
|
'status': ['ALL', 'all', 'fetch', 'short'],
|
|
82
|
+
'submodules': [
|
|
83
|
+
'abort',
|
|
84
|
+
'dry_run',
|
|
85
|
+
'gpg_sign',
|
|
86
|
+
'ignore',
|
|
87
|
+
'only',
|
|
88
|
+
'recursive',
|
|
89
|
+
'resolve_relative',
|
|
90
|
+
'resume',
|
|
91
|
+
'signoff',
|
|
92
|
+
'single_commit',
|
|
93
|
+
],
|
|
94
|
+
'verify': ['diffs', 'ignore', 'keep', 'output_dir', 'patches'],
|
|
79
95
|
'version': [],
|
|
80
96
|
}
|
|
81
97
|
|
|
@@ -91,11 +107,14 @@ COMMAND_HELP: dict[str, str] = {
|
|
|
91
107
|
'diff': "show a nested repository's local changes against upstream",
|
|
92
108
|
'fetch': "fetch upstream content for a nested repository",
|
|
93
109
|
'init': "turn an existing subdirectory into a nested repository",
|
|
110
|
+
'patches': "write, check, drop or cherry-pick the patches of a nested repository",
|
|
94
111
|
'pull': "update a nested repository from its upstream",
|
|
95
112
|
'push': "push a nested repository's local changes upstream",
|
|
96
113
|
'repair': "point a nested repository's sync point back into the current history",
|
|
97
114
|
'run': "run a git command in the worktree of a nested repository",
|
|
98
115
|
'status': "report on the nested repositories of this repository",
|
|
116
|
+
'submodules': "turn all submodules into nested repositories and keep them up to date",
|
|
117
|
+
'verify': "check that a nested repository is its upstream commit plus patches or diffs",
|
|
99
118
|
'version': "print version information",
|
|
100
119
|
}
|
|
101
120
|
|
|
@@ -135,18 +154,70 @@ SUBPARSER_ARG_SPECS: list[tuple[str, tuple[str, ...], dict]] = [
|
|
|
135
154
|
('-c', '--commit'),
|
|
136
155
|
{'action': 'store_true', 'help': "record the pushed commit in .gitnested; needs a push to the tracked branch"},
|
|
137
156
|
),
|
|
138
|
-
(
|
|
157
|
+
(
|
|
158
|
+
'dry_run',
|
|
159
|
+
('--dry-run',),
|
|
160
|
+
{'action': 'store_true', 'dest': 'dry_run', 'help': "show what would happen; change nothing"},
|
|
161
|
+
),
|
|
162
|
+
(
|
|
163
|
+
'diffs',
|
|
164
|
+
('--diffs',),
|
|
165
|
+
{'nargs': '*', 'metavar': 'DIFF', 'help': "check against these diffs, in order"},
|
|
166
|
+
),
|
|
167
|
+
(
|
|
168
|
+
'keep',
|
|
169
|
+
('-k', '--keep'),
|
|
170
|
+
{'action': 'store_true', 'help': "keep the temporary worktree"},
|
|
171
|
+
),
|
|
139
172
|
('force', ('-f', '--force'), {'action': 'store_true', 'help': "force the operation"}),
|
|
140
173
|
(
|
|
141
174
|
'from_submodule',
|
|
142
175
|
('--from-submodule',),
|
|
143
176
|
{'action': 'store_true', 'help': "turn the submodule at <subdir> into a nested repository"},
|
|
144
177
|
),
|
|
178
|
+
(
|
|
179
|
+
'local',
|
|
180
|
+
('--local',),
|
|
181
|
+
{'action': 'store_true', 'help': "cherry-pick: add the new patches to patches-local"},
|
|
182
|
+
),
|
|
183
|
+
(
|
|
184
|
+
'from_patches',
|
|
185
|
+
('--from-patches',),
|
|
186
|
+
{'action': 'store_true', 'help': "build the branch from upstream and the patches; check it gives <subdir>"},
|
|
187
|
+
),
|
|
145
188
|
(
|
|
146
189
|
'recurse_submodules',
|
|
147
190
|
('--recurse-submodules',),
|
|
148
191
|
{'action': 'store_true', 'help': "nest the submodules of upstream too, at the commits it pins"},
|
|
149
192
|
),
|
|
193
|
+
(
|
|
194
|
+
'ignore',
|
|
195
|
+
('--ignore',),
|
|
196
|
+
{
|
|
197
|
+
'action': 'append',
|
|
198
|
+
'metavar': 'PATH',
|
|
199
|
+
'help': "skip this path of <subdir>, just this time; submodules init: do not nest the submodules there",
|
|
200
|
+
},
|
|
201
|
+
),
|
|
202
|
+
(
|
|
203
|
+
'only',
|
|
204
|
+
('--only',),
|
|
205
|
+
{
|
|
206
|
+
'action': 'append',
|
|
207
|
+
'metavar': 'PATH',
|
|
208
|
+
'help': "init: nest only the submodules at or below this path; give it more than once for more",
|
|
209
|
+
},
|
|
210
|
+
),
|
|
211
|
+
(
|
|
212
|
+
'recursive',
|
|
213
|
+
('--recursive',),
|
|
214
|
+
{'action': 'store_true', 'help': "init: nest the submodules of the submodules too, at any depth"},
|
|
215
|
+
),
|
|
216
|
+
(
|
|
217
|
+
'resolve_relative',
|
|
218
|
+
('--resolve-relative',),
|
|
219
|
+
{'metavar': 'URL', 'help': "init: resolve relative submodule urls against this url"},
|
|
220
|
+
),
|
|
150
221
|
('exclude', ('--exclude',), {'action': 'append', 'help': "leave this path of the upstream repository out"}),
|
|
151
222
|
(
|
|
152
223
|
'gpg_sign',
|
|
@@ -186,10 +257,33 @@ SUBPARSER_ARG_SPECS: list[tuple[str, tuple[str, ...], dict]] = [
|
|
|
186
257
|
{'dest': 'name', 'help': "name of the nested branch; defaults to nested/<subdir>"},
|
|
187
258
|
),
|
|
188
259
|
('message', ('-m', '--message'), {'dest': 'message', 'help': "commit message"}),
|
|
260
|
+
(
|
|
261
|
+
'patches',
|
|
262
|
+
('--patches',),
|
|
263
|
+
{'nargs': '*', 'metavar': 'PATCH', 'help': "check against these patches, in order (the default)"},
|
|
264
|
+
),
|
|
189
265
|
('path', ('--path',), {'dest': 'path', 'help': "nest only this directory of upstream, at the root of <subdir>"}),
|
|
190
266
|
('pin', ('--pin',), {'dest': 'pin', 'help': "nest this commit or tag of the branch, not its newest commit"}),
|
|
191
267
|
('msg_file', ('--file',), {'dest': 'msg_file', 'help': "file to read the commit message from"}),
|
|
192
268
|
('tag', ('--tag',), {'dest': 'tag', 'help': "tag the pushed upstream commit with this name"}),
|
|
269
|
+
(
|
|
270
|
+
'output_dir',
|
|
271
|
+
('-o', '--output'),
|
|
272
|
+
{
|
|
273
|
+
'dest': 'output_dir',
|
|
274
|
+
'help': "patches: folder for the missing patch; diff: file for the whole diff",
|
|
275
|
+
},
|
|
276
|
+
),
|
|
277
|
+
(
|
|
278
|
+
'abort',
|
|
279
|
+
('--abort',),
|
|
280
|
+
{'action': 'store_true', 'help': "throw away a 'submodules update' that stopped on conflicts"},
|
|
281
|
+
),
|
|
282
|
+
(
|
|
283
|
+
'resume',
|
|
284
|
+
('--continue',),
|
|
285
|
+
{'action': 'store_true', 'dest': 'resume', 'help': "finish a 'submodules update' that stopped on conflicts"},
|
|
286
|
+
),
|
|
193
287
|
('remote', ('-r', '--remote'), {'dest': 'remote', 'help': "upstream remote to push/pull/fetch"}),
|
|
194
288
|
(
|
|
195
289
|
'short',
|
|
@@ -199,7 +293,7 @@ SUBPARSER_ARG_SPECS: list[tuple[str, tuple[str, ...], dict]] = [
|
|
|
199
293
|
(
|
|
200
294
|
'single_commit',
|
|
201
295
|
('--single-commit',),
|
|
202
|
-
{'action': 'store_true', 'help': "
|
|
296
|
+
{'action': 'store_true', 'help': "one commit for all nested repositories"},
|
|
203
297
|
),
|
|
204
298
|
(
|
|
205
299
|
'squash',
|
|
@@ -226,6 +320,7 @@ SUPPORTED_OPT_ATTRS: list[tuple[str, str]] = [
|
|
|
226
320
|
('path', 'path'),
|
|
227
321
|
('pin', 'pin'),
|
|
228
322
|
('tag', 'tag'),
|
|
323
|
+
('output_dir', 'output_dir'),
|
|
229
324
|
]
|
|
230
325
|
|
|
231
326
|
# Most commands take the same first positional, so it gets the same text.
|
|
@@ -255,6 +350,24 @@ POSITIONALS: dict[str, list[tuple[str, dict]]] = {
|
|
|
255
350
|
'diff': [('subdir', {'nargs': '?', 'help': _SUBDIR_HELP})],
|
|
256
351
|
'fetch': [('subdir', {'nargs': '?', 'help': _SUBDIR_HELP})],
|
|
257
352
|
'init': [('subdir', {'nargs': '?', 'help': "subdirectory to turn into a nested repository"})],
|
|
353
|
+
'patches': [
|
|
354
|
+
(
|
|
355
|
+
'action',
|
|
356
|
+
{
|
|
357
|
+
'choices': ['create', 'verify', 'drop', 'cherry-pick'],
|
|
358
|
+
'help': "create: write; verify: check; drop: remove patches; cherry-pick: add commits or patch files",
|
|
359
|
+
},
|
|
360
|
+
),
|
|
361
|
+
('subdir', {'nargs': '?', 'help': _SUBDIR_HELP}),
|
|
362
|
+
(
|
|
363
|
+
'patch_names',
|
|
364
|
+
{
|
|
365
|
+
'nargs': '*',
|
|
366
|
+
'metavar': 'patch',
|
|
367
|
+
'help': "drop: the patches, by path or file name; cherry-pick: commits or patch files",
|
|
368
|
+
},
|
|
369
|
+
),
|
|
370
|
+
],
|
|
258
371
|
'pull': [('subdir', {'nargs': '?', 'help': _SUBDIR_HELP})],
|
|
259
372
|
'push': [
|
|
260
373
|
('subdir', {'nargs': '?', 'help': _SUBDIR_HELP}),
|
|
@@ -267,6 +380,21 @@ POSITIONALS: dict[str, list[tuple[str, dict]]] = {
|
|
|
267
380
|
# everything after <subdir> goes to git untouched, options included.
|
|
268
381
|
('git_args', {'nargs': '...', 'help': "the git command to run, with its arguments"}),
|
|
269
382
|
],
|
|
383
|
+
'submodules': [
|
|
384
|
+
(
|
|
385
|
+
'submodules_action',
|
|
386
|
+
{'metavar': 'action', 'choices': ['init', 'update'], 'help': "init or update"},
|
|
387
|
+
),
|
|
388
|
+
(
|
|
389
|
+
'submodules_args',
|
|
390
|
+
{
|
|
391
|
+
'metavar': 'arg',
|
|
392
|
+
'nargs': '*',
|
|
393
|
+
'help': "update: the commit to merge",
|
|
394
|
+
},
|
|
395
|
+
),
|
|
396
|
+
],
|
|
397
|
+
'verify': [('subdir', {'help': _SUBDIR_HELP})],
|
|
270
398
|
}
|
|
271
399
|
|
|
272
400
|
# The paragraph `git nested <command> --help` opens with. COMMAND_HELP is
|
|
@@ -321,6 +449,14 @@ COMMAND_DESCRIPTION: dict[str, str] = {
|
|
|
321
449
|
"The files are left as they are. Without --remote the subdirectory has no upstream yet, "
|
|
322
450
|
"which you can add later with 'git nested config'."
|
|
323
451
|
),
|
|
452
|
+
'patches': (
|
|
453
|
+
"Keep the local changes of <subdir> as patch files, in the patches folder of .gitnested. "
|
|
454
|
+
"create writes the changes that are not in a patch yet as a new patch, with a TODO title, "
|
|
455
|
+
"and writes all patches again in the one form git-nested uses. "
|
|
456
|
+
"verify only checks: it fails if a change is not in a patch, or if create would change a patch. "
|
|
457
|
+
"drop removes patches and their changes from <subdir>, writes the other patches again and commits. "
|
|
458
|
+
"cherry-pick adds commits (fetched from the remote if needed) or patch files as patches, and commits."
|
|
459
|
+
),
|
|
324
460
|
'pull': (
|
|
325
461
|
"Fetch the upstream of <subdir> and join the new commits into it, by merge or by rebase. "
|
|
326
462
|
"By default the result is one parent commit; --no-squash keeps the upstream commits as a merged second-parent history. "
|
|
@@ -348,6 +484,28 @@ COMMAND_DESCRIPTION: dict[str, str] = {
|
|
|
348
484
|
"Report every nested repository in this repository: where it comes from, and whether it has local changes. "
|
|
349
485
|
"With -q, just the paths, one per line."
|
|
350
486
|
),
|
|
487
|
+
'submodules': (
|
|
488
|
+
"Turn the submodules of a repository into plain files. "
|
|
489
|
+
"Each submodule becomes a nested repository. "
|
|
490
|
+
"The repository itself stays a normal git clone of its upstream. "
|
|
491
|
+
"'init' nests all submodules at the commits they pin, one commit each. "
|
|
492
|
+
"With --recursive, their submodules too. "
|
|
493
|
+
"--only and --ignore pick some of them. "
|
|
494
|
+
"A relative submodule url needs --resolve-relative. "
|
|
495
|
+
"'update' merges an upstream commit and keeps its commits. It does not fetch. "
|
|
496
|
+
"Then it changes the nested repositories the way upstream changed its submodules. "
|
|
497
|
+
"Each changed submodule gets one commit. "
|
|
498
|
+
"On conflicts, fix them and run --continue. Or give up with --abort. "
|
|
499
|
+
"--dry-run shows the changes and makes none."
|
|
500
|
+
),
|
|
501
|
+
'verify': (
|
|
502
|
+
"Apply the files to the upstream commit in .gitnested, in a temporary worktree, and compare "
|
|
503
|
+
"the result with <subdir>. Patches go in with git am; plain diffs need --diffs. "
|
|
504
|
+
"Without files, check that <subdir> has no local changes. "
|
|
505
|
+
"If it differs, the command fails. With patches, it then writes the missing patch next to the last one "
|
|
506
|
+
"(or into --output). With --diffs, it writes the whole diff to --output. "
|
|
507
|
+
"For the patches folder of .gitnested, use 'git nested patches'."
|
|
508
|
+
),
|
|
351
509
|
'version': "Print the git-nested version, and the versions of git and Python it is running on.",
|
|
352
510
|
}
|
|
353
511
|
|
|
@@ -397,11 +555,21 @@ COMMAND_EXAMPLES: dict[str, list[str]] = {
|
|
|
397
555
|
"git nested init -r https://github.com/user/lib --commit abc123 ext/lib",
|
|
398
556
|
"git nested init -r https://github.com/user/lib -b main ext/lib",
|
|
399
557
|
],
|
|
558
|
+
'patches': [
|
|
559
|
+
"git nested patches create ext/lib",
|
|
560
|
+
"git nested patches verify ext/lib",
|
|
561
|
+
"git nested patches verify --all",
|
|
562
|
+
"git nested patches drop ext/lib 0002-Old-workaround.patch",
|
|
563
|
+
"git nested patches cherry-pick ext/lib a1b2c3d",
|
|
564
|
+
"git nested patches cherry-pick -r https://github.com/fork/lib --local ext/lib refs/pull/42/head",
|
|
565
|
+
"git nested patches cherry-pick ext/lib ~/fix-build.patch",
|
|
566
|
+
],
|
|
400
567
|
'pull': [
|
|
401
568
|
"git nested pull ext/lib",
|
|
402
569
|
"git nested pull --no-squash ext/lib",
|
|
403
570
|
"git nested pull -M rebase ext/lib",
|
|
404
571
|
"git nested pull --rebase ext/lib",
|
|
572
|
+
"git nested pull --ignore ext/lib/tests ext/lib",
|
|
405
573
|
"git nested pull --all",
|
|
406
574
|
"git nested pull --all -b release/2.0",
|
|
407
575
|
],
|
|
@@ -410,6 +578,7 @@ COMMAND_EXAMPLES: dict[str, list[str]] = {
|
|
|
410
578
|
"git nested push --dry-run ext/lib",
|
|
411
579
|
"git nested push --rebase ext/lib",
|
|
412
580
|
"git nested push --squash -m 'one commit upstream' ext/lib",
|
|
581
|
+
"git nested push --ignore ext/lib/local ext/lib",
|
|
413
582
|
"git nested push --all -b release/2.0",
|
|
414
583
|
],
|
|
415
584
|
'repair': [
|
|
@@ -424,6 +593,23 @@ COMMAND_EXAMPLES: dict[str, list[str]] = {
|
|
|
424
593
|
"git nested status",
|
|
425
594
|
"git nested status -q",
|
|
426
595
|
],
|
|
596
|
+
'submodules': [
|
|
597
|
+
"git nested submodules init",
|
|
598
|
+
"git nested submodules init --recursive",
|
|
599
|
+
"git nested submodules init --only lib/core",
|
|
600
|
+
"git nested submodules init --resolve-relative https://github.com/org/project",
|
|
601
|
+
"git nested submodules update upstream/main",
|
|
602
|
+
"git nested submodules update --dry-run upstream/main",
|
|
603
|
+
"git nested submodules update v2.0",
|
|
604
|
+
"git nested submodules update --continue",
|
|
605
|
+
],
|
|
606
|
+
'verify': [
|
|
607
|
+
"git nested verify ext/lib --patches patches/lib/*.patch",
|
|
608
|
+
"git nested verify -o patches/lib ext/lib",
|
|
609
|
+
"git nested verify ext/lib --diffs lib.diff",
|
|
610
|
+
"git nested verify -o lib.diff ext/lib --diffs lib.diff",
|
|
611
|
+
"git nested verify --ignore ext/lib/build ext/lib",
|
|
612
|
+
],
|
|
427
613
|
'version': [
|
|
428
614
|
"git nested version",
|
|
429
615
|
],
|
git_nested/commands/__init__.py
CHANGED
|
@@ -26,11 +26,14 @@ from . import (
|
|
|
26
26
|
diff,
|
|
27
27
|
fetch,
|
|
28
28
|
init,
|
|
29
|
+
patchset,
|
|
29
30
|
pull,
|
|
30
31
|
push,
|
|
31
32
|
repair,
|
|
32
33
|
run,
|
|
33
34
|
status,
|
|
35
|
+
submodules,
|
|
36
|
+
verify,
|
|
34
37
|
version,
|
|
35
38
|
)
|
|
36
39
|
|
|
@@ -44,10 +47,13 @@ REGISTRY: dict[str, Callable[[CommandContext], None]] = {
|
|
|
44
47
|
'diff': diff.cmd_diff,
|
|
45
48
|
'fetch': fetch.cmd_fetch,
|
|
46
49
|
'init': init.cmd_init,
|
|
50
|
+
'patches': patchset.cmd_patches,
|
|
47
51
|
'pull': pull.cmd_pull,
|
|
48
52
|
'push': push.cmd_push,
|
|
49
53
|
'repair': repair.cmd_repair,
|
|
50
54
|
'run': run.cmd_run,
|
|
51
55
|
'status': status.cmd_status,
|
|
56
|
+
'submodules': submodules.cmd_submodules,
|
|
57
|
+
'verify': verify.cmd_verify,
|
|
52
58
|
'version': version.cmd_version,
|
|
53
59
|
}
|