Skip to content

Conversation

@tony
Copy link
Member

@tony tony commented Jun 18, 2024

cmd(git): More commands

Summary

This PR introduces the Manager/Cmd pattern for git subcommands, providing
typed Python objects instead of raw strings. It also includes numerous bug
fixes to existing git command implementations.

Changes

New: Manager/Cmd Pattern

Manager (collection-level)              Cmd (per-entity)
├── ls() -> QueryList[Cmd]              ├── run()
├── get(**kwargs) -> Cmd                ├── show()
├── filter(**kwargs) -> list[Cmd]       ├── remove()/delete()
├── add() / create()                    └── entity-specific operations
└── run()

New Manager Classes

Manager Access Cmd Class
GitBranchManager git.branches GitBranchCmd
GitRemoteManager git.remotes GitRemoteCmd
GitStashManager git.stashes GitStashEntryCmd
GitTagManager git.tags GitTagCmd
GitWorktreeManager git.worktrees GitWorktreeCmd
GitNotesManager git.notes GitNoteCmd
GitSubmoduleManager git.submodules GitSubmoduleEntryCmd
GitReflogManager git.reflog GitReflogEntryCmd

Enhanced Git.init()

  • ref_format parameter for --ref-format (files/reftable)
  • make_parents to auto-create parent directories (default: True)
  • shared extended to support octal permissions (e.g., "0660")
  • Improved parameter validation with clear error messages

Bug Fixes

Git.run() fixes

  • Fix boolean config serialization ("true" if True -> "true" if v)
  • Fix -C paths iteration (was appending list instead of iterating)
  • Implement config_env parameter

Git.clone() fixes

  • Fix reference_if_able flag name (was using --reference)
  • Fix separate_git_dir quoting (was using !r instead of !s)

Git.fetch()/Git.pull() fixes

  • Remove clone-only branch/origin parameters
  • Implement recurse_submodules, recurse_submodules_default, negotiation_tip
  • Fix --signoff flag spelling

Git.rebase() fixes

  • Fix --no-rerere-autoupdate typo
  • Fix whitespace options (ignore_whitespace instead of no_whitespace)

Git.init() fixes

  • Remove invalid --default parameter
  • Don't pass --shared when shared=False

Manager/Cmd fixes

  • Forward **kwargs to underlying cmd.run() calls in all Manager/Cmd classes
  • GitRemoteManager.ls(): Handle URLs with spaces in regex
  • GitNoteCmd: Propagate custom ref to note commands
  • GitNotesManager.merge(): Handle commit/abort forms correctly
  • GitStashCmd.push(): Handle str paths correctly
  • stash.pop(): Use stash@{N} format instead of pathspec separator
  • branch.create(): Use git branch instead of checkout -b
  • remote.add(): Wire fetch, track, and master parameters
  • remote.show(): Only add --verbose when explicitly True

Documentation

  • API documentation for all new Manager/Cmd classes
  • Split git subcommand docs into separate pages
  • Usage examples in README and quickstart

@codecov
Copy link

codecov bot commented Jun 18, 2024

Codecov Report

❌ Patch coverage is 99.58203% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.18%. Comparing base (23ddb7e) to head (a402e4d).
⚠️ Report is 80 commits behind head on master.

Files with missing lines Patch % Lines
tests/cmd/test_git.py 99.57% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #465       +/-   ##
===========================================
+ Coverage   42.90%   53.18%   +10.28%     
===========================================
  Files          38       38               
  Lines        3627     5663     +2036     
  Branches      794     1063      +269     
===========================================
+ Hits         1556     3012     +1456     
- Misses       1718     2120      +402     
- Partials      353      531      +178     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony force-pushed the more-git-cmds branch 3 times, most recently from ff046f3 to 5dff81e Compare June 24, 2024 00:02
@tony tony force-pushed the more-git-cmds branch 8 times, most recently from 8808231 to 700a4fa Compare July 4, 2024 23:01
@tony tony force-pushed the more-git-cmds branch 3 times, most recently from f520132 to af58b49 Compare July 20, 2024 11:39
@tony tony force-pushed the more-git-cmds branch 2 times, most recently from adfbeaa to 76822c5 Compare August 3, 2024 12:20
@tony tony force-pushed the more-git-cmds branch 4 times, most recently from 8adff4c to c843022 Compare August 10, 2024 19:57
@tony tony force-pushed the more-git-cmds branch 3 times, most recently from 8839c2a to 0385215 Compare August 16, 2024 23:56
@tony tony force-pushed the more-git-cmds branch 2 times, most recently from 11dbd6a to 8578585 Compare August 26, 2024 10:50
@tony tony force-pushed the more-git-cmds branch 4 times, most recently from 2e5347d to bb6d40e Compare September 6, 2024 23:56
tony added 25 commits November 30, 2025 09:24
why: Both shared=True and shared=False were passing --shared flag.
Per git-init docs, --shared without value defaults to group permissions.
shared=False should mean "no --shared flag", not default group sharing.
what:
- Only append --shared when shared is explicitly True
- shared=False now behaves like shared=None (no flag)
why: Plain str paths were silently ignored when building required_flags.
Only list and pathlib.Path were handled, so Git.stash.push(path="file.txt")
would not limit the stash to that path.
what:
- Change elif isinstance(path, pathlib.Path) to elif path is not None
- Remove unnecessary .absolute() conversion that broke relative paths
- Now handles str, Path, and list consistently
why: GitRemoteManager.add() exposed fetch, track, master parameters in
the signature but they were never translated to CLI flags. Callers would
get silent no-ops when using these options.
what:
- Wire fetch to -f flag
- Wire track to -t <branch>
- Wire master to -m <master>
why: Commit 03b124c fixed shared=False passing --shared, but had no test.
what:
- Add InitSharedFixture NamedTuple with 4 test cases
- Test shared=True, shared=False, shared=None, shared="group"
- Verify "shared git repository" only in output when expected
why: Commit ece0d80 fixed str paths being silently ignored, but had no test.
what:
- Add test_stash_cmd_push_path_types testing str, pathlib.Path, list
- Use timestamp filenames to avoid pytest-rerunfailures conflicts
- Verify only specified file is stashed, not all modified files
…params

why: Commit 5c00880 wired fetch/track/master params, but had no test.
what:
- Add RemoteAddParamFixture NamedTuple with 3 test cases
- Add CreateRepoPytestFixtureFn import for remote repo fixture
- Test fetch=True, track="master", master="master" separately
why: Git's `notes merge` has three mutually exclusive forms that were
not properly supported - the API required notes_ref always, but --commit
and --abort forms take no ref argument.
what:
- Make notes_ref optional (None default)
- Add validation for mutual exclusivity of commit, abort, and notes_ref
- Only append notes_ref when provided and not using commit/abort
- Prevent strategy from being used with commit/abort
why: Multiple run() methods accepted **kwargs in their signatures but
did not forward them to self.cmd.run(), silently ignoring any extra
parameters like env, cwd, or custom config.
what:
- Add **kwargs to GitNotesManager.run() call
- Add **kwargs to GitBranchCmd.run() call
- Add **kwargs to GitBranchManager.run() call
- Add **kwargs to GitRemoteCmd.run() call
- Add **kwargs to GitRemoteManager.run() call
why: The assert statement was checking `any(f for f in ["push", "fetch"])`
which always returns True since non-empty strings are truthy. The actual
mirror value was never validated.
what:
- Replace always-true assertion with proper validation
- Raise ValueError if mirror is not 'push' or 'fetch'
- Fix --mirror syntax to use = format per git-remote docs
- Simplify bool check with elif
…eter

why: The doctest used quiet=True, but git remote has no --quiet flag.
With kwargs passthrough now working, this caused a TypeError.
what:
- Replace quiet=True with check_returncode=False in doctest
why: The -C flag was appending a list object as the second element,
creating invalid argv like ["git", "-C", ["path1", "path2"]] which
subprocess cannot handle. Git requires separate -C flags for each path.
what:
- Iterate over normalized C list and append -C for each path
why: The stringify helper used `if True` instead of `if v`, causing all
boolean config values to serialize as "true" regardless of actual value.
what:
- Change `return "true" if True else "false"` to `if v else "false"`
why: Git's init command does not have a --default flag. This parameter
would cause runtime failures when used.
what:
- Remove default parameter from init() signature
- Remove default from docstring
- Remove if default is True: block
why: Using `if verbose is not None:` would add --verbose for both
True and False values. The flag should only be added when verbose=True.
what:
- Change `if verbose is not None:` to `if verbose is True:` in
  GitRemoteCmd.show() and GitRemoteManager.show()
why: The config_env parameter was accepted but never used, silently
ignoring callers who expected --config-env=<name>=<envvar> to be passed.
what:
- Add implementation to emit --config-env flag after config handling
why: Using !r (repr) incorrectly added quotes around the path, causing
git to fail with "unknown option: --separate-git-dir='/path/to/dir'".
what:
- Change {separate_git_dir!r} to {separate_git_dir!s} to match init()
why: The parameter was emitting --reference instead of --reference-if-able,
which has different semantics (--reference fails if local repo is missing,
--reference-if-able silently skips).
what:
- Change ["--reference", reference_if_able] to ["--reference-if-able", ...]
why: The --branch and --origin flags only exist for git clone, not for
git fetch or git pull. Using these parameters would cause git to fail
with "unknown option 'branch'" or "unknown option 'origin'".
what:
- Remove branch and origin parameters from fetch() signature
- Remove branch and origin parameters from pull() signature
- Remove implementation code that emitted these invalid flags
why: Parameters deepen, negotiation_tip, recurse_submodules, and
recurse_submodules_default were defined but never used, silently
ignoring callers' requests.
what:
- Add --deepen=<n> flag emission for deepen parameter
- Add --negotiation-tip=<rev> flag emission for negotiation_tip
- Add --recurse-submodules[=yes|on-demand|no] for recurse_submodules
- Add --recurse-submodules-default=<value> for recurse_submodules_default
- Applied to both fetch() and pull() methods
why: The whitespace parameter emitted --whitespace without the required
value, and no_whitespace was invalid (git has no such flag). The correct
options are --whitespace=<option> and --ignore-whitespace.
what:
- Fix whitespace to emit --whitespace=<value> with the option value
- Replace no_whitespace with ignore_whitespace parameter
- Emit --ignore-whitespace for the new boolean parameter
why: The flag was misspelled as --no-rerwre-autoupdate which would cause
git to fail with "unknown option".
what:
- Fix typo: rerwre -> rerere
why: The flags were spelled with underscore (--sign_off, --no-sign_off)
but git expects hyphen format (--signoff, --no-signoff).
what:
- Change --sign_off to --signoff
- Change --no-sign_off to --no-signoff
why: GitNotesManager stores a custom ref but GitNoteCmd objects
created by ls() ignored it, causing show/remove operations to
always target refs/notes/commits instead of the manager's ref.
what:
- Add ref parameter to GitNoteCmd.__init__
- Pass --ref flag in GitNoteCmd.run() when ref is set
- Update GitNotesManager.ls() to pass ref to GitNoteCmd instances
why: The regex pattern used \S+ for URLs which failed to match
paths containing spaces (e.g., /tmp/foo bar), causing those
remotes to be silently dropped from the listing.
what:
- Change URL pattern from \S+ to .+? (non-greedy any char)
- Add line anchors (^ and $) for proper multiline matching
- Non-greedy ensures we stop at the (fetch|push) suffix
why: Ensure GitNoteCmd ref propagation and URL space parsing fixes
are covered by explicit regression tests.
what:
- Add test_notes_custom_ref_propagation: verifies GitNoteCmd objects
  from ls() inherit the manager's ref for show/remove operations
- Add test_remote_url_with_spaces: verifies regex correctly parses
  remote URLs containing spaces (e.g., /tmp/foo bar)
- Both tests use parametrized NamedTuple fixtures per project pattern
@tony
Copy link
Member Author

tony commented Nov 30, 2025

Code review

Found 4 issues:

  1. GitTagCmd.run() accepts **kwargs in signature but does not forward them to self.cmd.run(), causing silent parameter loss

libvcs/src/libvcs/cmd/git.py

Lines 5941 to 5947 in 9421936

return self.cmd.run(
["tag", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitTagManager.run() accepts **kwargs in signature but does not forward them to self.cmd.run(), causing silent parameter loss

libvcs/src/libvcs/cmd/git.py

Lines 6121 to 6127 in 9421936

return self.cmd.run(
["tag", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitWorktreeManager.run() accepts **kwargs in signature but does not forward them to self.cmd.run(), causing silent parameter loss

libvcs/src/libvcs/cmd/git.py

Lines 6695 to 6701 in 9421936

return self.cmd.run(
["worktree", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitStashEntryCmd.run() accepts **kwargs in signature but does not forward them to self.cmd.run(), causing silent parameter loss

libvcs/src/libvcs/cmd/git.py

Lines 5553 to 5559 in 9421936

*,
# Pass-through to run()
log_in_real_time: bool = False,
check_returncode: bool | None = None,
) -> str:
"""Remove the upstream (tracking) information.

Context: Commit 7cf3f46 fixed this exact pattern in other Manager/Cmd classes (GitRemoteCmd, GitRemoteManager, GitBranchCmd, GitBranchManager, GitNotesManager), but these classes were missed. The correct pattern is shown in GitSubmoduleEntryCmd.run() (lines 2670-2675) where **kwargs is forwarded to self.cmd.run().

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@tony
Copy link
Member Author

tony commented Nov 30, 2025

Code review (updated)

Found 7 instances of missing **kwargs forwarding. These methods accept **kwargs in their signature but do not forward them to self.cmd.run(), causing silent parameter loss:

  1. GitSubmoduleCmd.run() - lines 2451-2455

libvcs/src/libvcs/cmd/git.py

Lines 2450 to 2456 in 9421936

return self.cmd.run(
["submodule", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitStashEntryCmd.run() - lines 4554-4558

libvcs/src/libvcs/cmd/git.py

Lines 4553 to 4559 in 9421936

return self.cmd.run(
["stash", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitStashManager.run() - lines 5076-5080

libvcs/src/libvcs/cmd/git.py

Lines 5075 to 5081 in 9421936

return self.cmd.run(
["stash", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitTagCmd.run() - lines 5942-5946

libvcs/src/libvcs/cmd/git.py

Lines 5941 to 5947 in 9421936

return self.cmd.run(
["tag", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitTagManager.run() - lines 6122-6126

libvcs/src/libvcs/cmd/git.py

Lines 6121 to 6127 in 9421936

return self.cmd.run(
["tag", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitWorktreeCmd.run() - lines 6459-6463

libvcs/src/libvcs/cmd/git.py

Lines 6458 to 6464 in 9421936

return self.cmd.run(
["worktree", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

  1. GitWorktreeManager.run() - lines 6696-6700

libvcs/src/libvcs/cmd/git.py

Lines 6695 to 6701 in 9421936

return self.cmd.run(
["worktree", *local_flags],
check_returncode=check_returncode,
log_in_real_time=log_in_real_time,
)

Context: Commit 7cf3f46 established the pattern of forwarding **kwargs to self.cmd.run() for GitRemoteCmd, GitRemoteManager, GitBranchCmd, GitBranchManager, and GitNotesManager. The correct pattern is shown in GitSubmoduleEntryCmd.run() lines 2670-2675.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

why: Multiple run() methods accepted **kwargs in their signatures but
did not forward them to self.cmd.run(), silently ignoring parameters
like env, cwd, or custom config.
what:
- Add **kwargs forwarding to GitSubmoduleCmd.run()
- Add **kwargs forwarding to GitStashEntryCmd.run()
- Add **kwargs forwarding to GitStashManager.run()
- Add **kwargs forwarding to GitTagCmd.run()
- Add **kwargs forwarding to GitTagManager.run()
- Add **kwargs forwarding to GitWorktreeCmd.run()
- Add **kwargs forwarding to GitWorktreeManager.run()
@tony tony merged commit d4e973e into master Nov 30, 2025
8 checks passed
@tony tony deleted the more-git-cmds branch November 30, 2025 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants