Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions doc/architecture/diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,62 @@ sequenceDiagram

---

## verify

```mermaid
sequenceDiagram
participant User
participant git-nested
participant Git

User->>git-nested: git nested verify <subdir> patches <patch>...
git-nested->>Git: rev-parse --show-prefix
Note over git-nested: Below the top level: rev-parse --show-toplevel,<br/>paths rebased, then run from there
git-nested->>Git: rev-parse --git-dir
git-nested->>Git: rev-parse --git-dir
git-nested->>Git: symbolic-ref --short --quiet HEAD
git-nested->>Git: rev-parse --is-inside-work-tree
git-nested->>Git: rev-parse HEAD
Note over git-nested: Read .gitnested config

git-nested->>Git: rev-parse HEAD:<subdir>
git-nested->>Git: rev-list <commit>^{commit} -1
opt upstream commit not here
git-nested->>Git: fetch --no-tags --quiet <remote> <branch>
git-nested->>Git: rev-parse FETCH_HEAD^0
git-nested->>Git: update-ref refs/nested/<subref>/fetch FETCH_HEAD^0
end
opt path, filter or exclude configured
Note over git-nested: Build a commit with only the nested part of upstream
git-nested->>Git: commit-tree -p <commit> <tree>
end

git-nested->>Git: worktree add --detach --no-checkout .git/tmp/verify/<subref> <base>
git-nested->>Git: reset -q --hard (in the worktree)
loop For each patch or diff
git-nested->>Git: apply --index <file> (in the worktree)
end
git-nested->>Git: commit --allow-empty --no-verify (in the worktree)
git-nested->>Git: diff --quiet HEAD <local tree> -- . ':(exclude,glob)**/.gitnested*'

opt it differs
git-nested->>Git: diff --stat HEAD <local tree>
alt patches
git-nested->>Git: diff --binary --full-index HEAD <local tree> | apply --index
git-nested->>Git: commit -m 'TODO: title' -m 'TODO: description'
git-nested->>Git: format-patch -1 --start-number=<n> -o <dir> HEAD
else diff with --output
git-nested->>Git: diff --binary <base> <local tree>
Note over git-nested: Written to the --output file
end
end

git-nested->>Git: worktree prune
Note over git-nested: The worktree is removed, unless --keep
```

---

## clean

```mermaid
Expand Down
1 change: 1 addition & 0 deletions doc/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Each command has its own page. This page lists what they share.
| [`fetch`](fetch.md) | fetch upstream content for a nested repository |
| [`status`](status.md) | report on the nested repositories of this repository |
| [`diff`](diff.md) | show a nested repository's local changes against upstream |
| [`verify`](verify.md) | check that a nested repository is its upstream commit plus patches or diffs |
| [`branch`](branch.md) | create a branch holding the local nested commits |
| [`run`](run.md) | run a git command in the worktree of a nested repository |
| [`commit`](commit.md) | add a nested branch to the current history as one commit |
Expand Down
60 changes: 60 additions & 0 deletions doc/cli/verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# `git nested verify`

Check that a nested repository is its upstream commit plus patches or diffs.

```text
git nested verify [<options>] <subdir> patches [--] [<patch>...]
git nested verify [<options>] <subdir> diff [--] [<diff>...]
```

## What it does

1. Checks out the upstream commit from `.gitnested` in a temporary
worktree. Fetches it only if it is missing.
2. Applies the files there, in order, with `git apply`.
3. Compares the result with `<subdir>` in `HEAD`. `.gitnested` files are
left out.

Without files, it checks that `<subdir>` has no local changes.

The files can be git patches (`git format-patch`) or plain diffs
(`git diff`, `diff -u`). Paths are relative to `<subdir>`.

Your working tree and branches are not touched.

### When it differs

The command fails and lists the changed files.

- `patches`: it writes the missing patch, numbered after the others, next
to the last patch or into `--output`. Fill in its TODO title and
description, rename it, and commit it.
- `diff`: with `--output`, it writes the whole diff to that file.

## Arguments

| Argument | Meaning |
|---|---|
| `<subdir>` | the nested repository |
| `patches` or `diff` | the kind of files |
| `<patch>`, `<diff>` | the files to apply, in order |

## Options

| Option | Meaning |
|---|---|
| `-o`, `--output <path>` | `patches`: folder for the missing patch. `diff`: file for the whole diff |
| `-k`, `--keep` | keep the temporary worktree |

## Examples

```bash
git nested verify ext/lib patches -- patches/lib/*.patch
git nested verify -o patches/lib ext/lib patches
git nested verify ext/lib diff lib.diff
git nested verify -o lib.diff ext/lib diff lib.diff
```

## See also

- [`diff`](diff.md)
2 changes: 2 additions & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The full reference of the command line. There is one page per command.
| [`fetch`](cli/fetch.md) | Download upstream changes, change no file |
| [`status`](cli/status.md) | Show all nested repositories |
| [`diff`](cli/diff.md) | Compare a nested repository with upstream |
| [`verify`](cli/verify.md) | Check local changes against patch files |
| [`branch`](cli/branch.md) | Build a branch with the nested history |
| [`run`](cli/run.md) | Run git in that branch's worktree |
| [`commit`](cli/commit.md) | Add that branch back as one commit |
Expand All @@ -99,6 +100,7 @@ cli/push
cli/fetch
cli/status
cli/diff
cli/verify
cli/branch
cli/run
cli/commit
Expand Down
3 changes: 3 additions & 0 deletions man/man1/git-nested.1
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ run a git command in the worktree of a nested repository
.B status
report on the nested repositories of this repository
.TP
.B verify
check that a nested repository is its upstream commit plus patches or diffs
.TP
.B version
print version information
.PP
Expand Down
2 changes: 1 addition & 1 deletion src/git_nested/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def check_worktree_clean(git: GitRunner, command: str) -> None:

def warn_uncommitted_subdir_changes(git: GitRunner, command: str, flags: Flags, subdir: Path) -> None:
"""Warn when `command` ignores uncommitted changes in `subdir`."""
if command not in ['branch', 'diff', 'push'] or (command == 'push' and flags.commit):
if command not in ['branch', 'diff', 'push', 'verify'] or (command == 'push' and flags.commit):
return

git.run(['update-index', '-q', '--ignore-submodules', '--refresh'], may_fail=True)
Expand Down
7 changes: 7 additions & 0 deletions src/git_nested/cli/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def at_top_level(git: GitRunner, command: str, ctx: CommandContext) -> AbstractC
# Path options are made absolute, so moving to the top level does not change what they name.
ctx.flags.dir = _absolute(ctx.flags.dir)
ctx.flags.message_file = _absolute(ctx.flags.message_file)
ctx.flags.output_dir = _absolute(ctx.flags.output_dir)
ctx.patches = _all_absolute(ctx.patches)
# Remotes given as relative paths are rebased onto the top level instead.
# Relative rather than absolute: a remote ends up in .gitnested, and a
# relative one keeps working in every clone of the parent repository.
Expand All @@ -73,6 +75,11 @@ def _absolute(path: str | None) -> str | None:
return os.path.abspath(path) if path else None # noqa: PTH100


def _all_absolute(paths: list[str] | None) -> list[str] | None:
"""Each of `paths` made absolute."""
return [os.path.abspath(path) for path in paths] if paths else paths # noqa: PTH100


def _remote_from(prefix: str, remote: str | None) -> str | None:
"""`remote`, typed below the top level at `prefix`, as seen from the top level.

Expand Down
3 changes: 3 additions & 0 deletions src/git_nested/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def _flags_from_args(args: argparse.Namespace) -> Flags:
no_squash=getattr(args, 'no_squash', False),
squash=getattr(args, 'squash', False),
update=getattr(args, 'update', False),
keep=getattr(args, 'keep', False),
quiet=getattr(args, 'quiet', False),
# argparse's `count` action leaves the attribute at None when the flag
# is absent, and the output layer wants a real count.
Expand Down Expand Up @@ -150,5 +151,7 @@ def parse_args(git: GitRunner, args_list: list[str]) -> tuple[str, CommandContex
config_key=getattr(args, 'key', None),
config_value=getattr(args, 'value', None),
git_args=getattr(args, 'git_args', None),
patches=getattr(args, 'patches', None),
verify_mode=getattr(args, 'verify_mode', None),
)
return args.command, context
2 changes: 1 addition & 1 deletion src/git_nested/cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def _check_existing_worktree(git: GitRunner, command: str, subdir: Path, gitnest
output.error(
f"{subdir.as_posix()}: no worktree to commit from; run 'git nested branch {subdir.as_posix()}' first"
)
elif command not in ['branch', 'clean', 'commit', 'push', 'run'] and worktree_path is not None:
elif command not in ['branch', 'clean', 'commit', 'push', 'run', 'verify'] and worktree_path is not None:
output.error(messages.worktree_exists(subdir, worktree_path, prunable=gitnested.exists()))


Expand Down
36 changes: 35 additions & 1 deletion src/git_nested/cli/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from __future__ import annotations

from ..constants import COMPLETION_SHELLS
from ..constants import COMPLETION_SHELLS, VERIFY_MODES

# Which flags each command accepts. The keys are also the command list:
# the subparsers, and the completion candidates, are built from them.
Expand Down Expand Up @@ -76,6 +76,7 @@
'update',
],
'status': ['ALL', 'all', 'fetch', 'short'],
'verify': ['keep', 'output_dir'],
'version': [],
}

Expand All @@ -96,6 +97,7 @@
'repair': "point a nested repository's sync point back into the current history",
'run': "run a git command in the worktree of a nested repository",
'status': "report on the nested repositories of this repository",
'verify': "check that a nested repository is its upstream commit plus patches or diffs",
'version': "print version information",
}

Expand Down Expand Up @@ -136,6 +138,11 @@
{'action': 'store_true', 'help': "record the pushed commit in .gitnested; needs a push to the tracked branch"},
),
('dry_run', ('--dry-run',), {'action': 'store_true', 'dest': 'dry_run', 'help': "show what would be pushed"}),
(
'keep',
('-k', '--keep'),
{'action': 'store_true', 'help': "keep the temporary worktree"},
),
('force', ('-f', '--force'), {'action': 'store_true', 'help': "force the operation"}),
(
'from_submodule',
Expand Down Expand Up @@ -190,6 +197,14 @@
('pin', ('--pin',), {'dest': 'pin', 'help': "nest this commit or tag of the branch, not its newest commit"}),
('msg_file', ('--file',), {'dest': 'msg_file', 'help': "file to read the commit message from"}),
('tag', ('--tag',), {'dest': 'tag', 'help': "tag the pushed upstream commit with this name"}),
(
'output_dir',
('-o', '--output'),
{
'dest': 'output_dir',
'help': "patches: folder for the missing patch; diff: file for the whole diff",
},
),
('remote', ('-r', '--remote'), {'dest': 'remote', 'help': "upstream remote to push/pull/fetch"}),
(
'short',
Expand Down Expand Up @@ -226,6 +241,7 @@
('path', 'path'),
('pin', 'pin'),
('tag', 'tag'),
('output_dir', 'output_dir'),
]

# Most commands take the same first positional, so it gets the same text.
Expand Down Expand Up @@ -267,6 +283,11 @@
# everything after <subdir> goes to git untouched, options included.
('git_args', {'nargs': '...', 'help': "the git command to run, with its arguments"}),
],
'verify': [
('subdir', {'help': _SUBDIR_HELP}),
('verify_mode', {'choices': list(VERIFY_MODES), 'help': "what the files are: numbered patches, or diffs"}),
('patches', {'nargs': '*', 'help': "the patches or diffs to apply, in order"}),
],
}

# The paragraph `git nested <command> --help` opens with. COMMAND_HELP is
Expand Down Expand Up @@ -348,6 +369,13 @@
"Report every nested repository in this repository: where it comes from, and whether it has local changes. "
"With -q, just the paths, one per line."
),
'verify': (
"Apply the files to the upstream commit in .gitnested, in a temporary worktree, and compare "
"the result with <subdir>. The files can be git patches or plain diffs. "
"Without files, check that <subdir> has no local changes. "
"If it differs, the command fails. 'patches' then writes the missing patch next to the last one "
"(or into --output); 'diff' writes the whole diff to --output."
),
'version': "Print the git-nested version, and the versions of git and Python it is running on.",
}

Expand Down Expand Up @@ -424,6 +452,12 @@
"git nested status",
"git nested status -q",
],
'verify': [
"git nested verify ext/lib patches -- patches/lib/*.patch",
"git nested verify -o patches/lib ext/lib patches",
"git nested verify ext/lib diff lib.diff",
"git nested verify -o lib.diff ext/lib diff lib.diff",
],
'version': [
"git nested version",
],
Expand Down
2 changes: 2 additions & 0 deletions src/git_nested/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
repair,
run,
status,
verify,
version,
)

Expand All @@ -49,5 +50,6 @@
'repair': repair.cmd_repair,
'run': run.cmd_run,
'status': status.cmd_status,
'verify': verify.cmd_verify,
'version': version.cmd_version,
}
Loading
Loading