git-nested 1.1.0__tar.gz → 1.1.1__tar.gz

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.
Files changed (131) hide show
  1. {git_nested-1.1.0 → git_nested-1.1.1}/.fish.rc +4 -3
  2. git_nested-1.1.1/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  3. git_nested-1.1.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. git_nested-1.1.1/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  5. git_nested-1.1.1/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  6. git_nested-1.1.1/.github/dependabot.yml +23 -0
  7. git_nested-1.1.1/.github/release.yml +22 -0
  8. git_nested-1.1.1/.github/workflows/build-binary.yml +137 -0
  9. {git_nested-1.1.0 → git_nested-1.1.1}/.github/workflows/publish.yml +3 -3
  10. {git_nested-1.1.0 → git_nested-1.1.1}/.github/workflows/release-binary.yml +41 -19
  11. {git_nested-1.1.0 → git_nested-1.1.1}/.github/workflows/test-binary.yml +6 -6
  12. {git_nested-1.1.0 → git_nested-1.1.1}/.github/workflows/test.yml +40 -11
  13. {git_nested-1.1.0 → git_nested-1.1.1}/.gitignore +1 -1
  14. {git_nested-1.1.0 → git_nested-1.1.1}/.pre-commit-config.yaml +5 -4
  15. {git_nested-1.1.0 → git_nested-1.1.1}/.rc +8 -2
  16. git_nested-1.1.1/CONTRIBUTING.md +95 -0
  17. {git_nested-1.1.0 → git_nested-1.1.1}/PKG-INFO +145 -102
  18. {git_nested-1.1.0 → git_nested-1.1.1}/README.md +141 -98
  19. git_nested-1.1.1/SECURITY.md +34 -0
  20. git_nested-1.1.1/bin/git-nested +39 -0
  21. {git_nested-1.1.0 → git_nested-1.1.1}/docs/diagrams.md +113 -5
  22. git_nested-1.1.1/man/man1/git-nested.1 +138 -0
  23. git_nested-1.1.1/pyproject.toml +221 -0
  24. git_nested-1.1.1/scripts/create-python-exe.sh +257 -0
  25. {git_nested-1.1.0 → git_nested-1.1.1}/sonar-project.properties +3 -3
  26. git_nested-1.1.1/src/git_nested/__init__.py +40 -0
  27. git_nested-1.1.1/src/git_nested/__main__.py +10 -0
  28. git_nested-1.1.1/src/git_nested/_compat.py +32 -0
  29. git_nested-1.1.1/src/git_nested/_version.py +21 -0
  30. git_nested-1.1.1/src/git_nested/checks.py +104 -0
  31. git_nested-1.1.1/src/git_nested/cli/__init__.py +3 -0
  32. git_nested-1.1.1/src/git_nested/cli/app.py +109 -0
  33. git_nested-1.1.1/src/git_nested/cli/main.py +20 -0
  34. git_nested-1.1.1/src/git_nested/cli/parser.py +170 -0
  35. git_nested-1.1.1/src/git_nested/cli/setup.py +103 -0
  36. git_nested-1.1.1/src/git_nested/cli/spec.py +243 -0
  37. git_nested-1.1.1/src/git_nested/commands/__init__.py +49 -0
  38. git_nested-1.1.1/src/git_nested/commands/branch.py +37 -0
  39. git_nested-1.1.1/src/git_nested/commands/clean.py +55 -0
  40. git_nested-1.1.1/src/git_nested/commands/clone.py +160 -0
  41. git_nested-1.1.1/src/git_nested/commands/commit.py +46 -0
  42. git_nested-1.1.1/src/git_nested/commands/completion.py +14 -0
  43. git_nested-1.1.1/src/git_nested/commands/config.py +104 -0
  44. git_nested-1.1.1/src/git_nested/commands/diff.py +53 -0
  45. git_nested-1.1.1/src/git_nested/commands/fetch.py +49 -0
  46. git_nested-1.1.1/src/git_nested/commands/init.py +81 -0
  47. git_nested-1.1.1/src/git_nested/commands/pull.py +234 -0
  48. git_nested-1.1.1/src/git_nested/commands/push.py +329 -0
  49. git_nested-1.1.1/src/git_nested/commands/status.py +184 -0
  50. git_nested-1.1.1/src/git_nested/commands/version.py +23 -0
  51. git_nested-1.1.1/src/git_nested/completion/__init__.py +278 -0
  52. git_nested-1.1.1/src/git_nested/completion/scripts.py +210 -0
  53. git_nested-1.1.1/src/git_nested/constants.py +13 -0
  54. git_nested-1.1.1/src/git_nested/content.py +199 -0
  55. git_nested-1.1.1/src/git_nested/discovery.py +76 -0
  56. git_nested-1.1.1/src/git_nested/errors.py +18 -0
  57. git_nested-1.1.1/src/git_nested/filters.py +164 -0
  58. git_nested-1.1.1/src/git_nested/git.py +104 -0
  59. git_nested-1.1.1/src/git_nested/gitfile.py +194 -0
  60. git_nested-1.1.1/src/git_nested/history.py +224 -0
  61. git_nested-1.1.1/src/git_nested/messages.py +95 -0
  62. git_nested-1.1.1/src/git_nested/models.py +114 -0
  63. git_nested-1.1.1/src/git_nested/output.py +172 -0
  64. git_nested-1.1.1/src/git_nested/py.typed +0 -0
  65. git_nested-1.1.1/src/git_nested/refs.py +58 -0
  66. git_nested-1.1.1/src/git_nested/repo.py +56 -0
  67. git_nested-1.1.1/src/git_nested/worktree.py +40 -0
  68. git_nested-1.1.1/src/git_nested/yamlio.py +22 -0
  69. {git_nested-1.1.0/lib → git_nested-1.1.1/src}/git_nested.egg-info/PKG-INFO +145 -102
  70. git_nested-1.1.1/src/git_nested.egg-info/SOURCES.txt +113 -0
  71. git_nested-1.1.1/src/git_nested.egg-info/requires.txt +1 -0
  72. git_nested-1.1.1/src/git_nested.egg-info/scm_file_list.json +109 -0
  73. git_nested-1.1.1/src/git_nested.egg-info/scm_version.json +8 -0
  74. {git_nested-1.1.0/test → git_nested-1.1.1/tests}/conftest.py +102 -35
  75. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/scenarios/test_extraction.py +5 -6
  76. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/scenarios/test_filter.py +2 -1
  77. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_branch.py +28 -19
  78. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_clean.py +1 -1
  79. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_clone.py +18 -20
  80. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_commit.py +2 -2
  81. git_nested-1.1.1/tests/e2e/test_completion.py +201 -0
  82. git_nested-1.1.1/tests/e2e/test_config.py +125 -0
  83. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_diff.py +5 -13
  84. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_encode.py +4 -5
  85. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_error.py +28 -30
  86. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_fetch.py +2 -3
  87. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_gitignore.py +1 -1
  88. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_init.py +11 -13
  89. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_nested_in_nested.py +6 -16
  90. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_pull.py +32 -41
  91. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_push.py +26 -28
  92. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_rebase.py +9 -7
  93. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_reclone.py +3 -5
  94. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_status.py +3 -2
  95. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/test_submodule.py +1 -1
  96. git_nested-1.1.1/tests/e2e/test_version.py +28 -0
  97. git_nested-1.1.1/tests/unit/conftest.py +37 -0
  98. git_nested-1.1.1/tests/unit/fakes.py +78 -0
  99. {git_nested-1.1.0/test/unittest → git_nested-1.1.1/tests/unit}/test_command.py +33 -26
  100. git_nested-1.1.1/tests/unit/test_completion_engine.py +321 -0
  101. git_nested-1.1.1/tests/unit/test_docs.py +39 -0
  102. git_nested-1.1.1/tests/unit/test_facade.py +97 -0
  103. {git_nested-1.1.0/test/unittest → git_nested-1.1.1/tests/unit}/test_helpers.py +78 -47
  104. git_nested-1.1.1/tests/unit/test_logging.py +193 -0
  105. {git_nested-1.1.0/test/unittest → git_nested-1.1.1/tests/unit}/test_repo_business.py +70 -93
  106. {git_nested-1.1.0 → git_nested-1.1.1}/uv.lock +83 -829
  107. git_nested-1.1.0/.github/workflows/build-binary.yml +0 -78
  108. git_nested-1.1.0/CONTRIBUTING.md +0 -229
  109. git_nested-1.1.0/lib/git-nested +0 -13
  110. git_nested-1.1.0/lib/git_nested.egg-info/SOURCES.txt +0 -60
  111. git_nested-1.1.0/lib/git_nested.egg-info/requires.txt +0 -1
  112. git_nested-1.1.0/lib/git_nested.py +0 -2690
  113. git_nested-1.1.0/pyproject.toml +0 -156
  114. git_nested-1.1.0/scripts/create-python-exe.sh +0 -189
  115. git_nested-1.1.0/share/completion.bash +0 -44
  116. git_nested-1.1.0/share/enable-completion.sh +0 -35
  117. git_nested-1.1.0/share/git-nested.fish +0 -35
  118. git_nested-1.1.0/share/zsh-completion/_git-nested +0 -72
  119. git_nested-1.1.0/test/test_completion.py +0 -144
  120. git_nested-1.1.0/test/test_version.py +0 -13
  121. git_nested-1.1.0/test/unittest/conftest.py +0 -62
  122. git_nested-1.1.0/test/unittest/test_logging.py +0 -159
  123. {git_nested-1.1.0 → git_nested-1.1.1}/.codecov.yml +0 -0
  124. {git_nested-1.1.0 → git_nested-1.1.1}/.gitattributes +0 -0
  125. {git_nested-1.1.0 → git_nested-1.1.1}/.github/CODEOWNERS +0 -0
  126. {git_nested-1.1.0 → git_nested-1.1.1}/LICENSE +0 -0
  127. {git_nested-1.1.0 → git_nested-1.1.1}/setup.cfg +0 -0
  128. {git_nested-1.1.0/lib → git_nested-1.1.1/src}/git_nested.egg-info/dependency_links.txt +0 -0
  129. {git_nested-1.1.0/lib → git_nested-1.1.1/src}/git_nested.egg-info/entry_points.txt +0 -0
  130. {git_nested-1.1.0/lib → git_nested-1.1.1/src}/git_nested.egg-info/top_level.txt +0 -0
  131. {git_nested-1.1.0/test → git_nested-1.1.1/tests/e2e}/scenarios/test_filemode.py +0 -0
@@ -11,7 +11,8 @@
11
11
  #------------------------------------------------------------------------------
12
12
 
13
13
  set GIT_NESTED_ROOT (dirname (realpath (status --current-filename)))
14
- set PATH $GIT_NESTED_ROOT/lib $PATH
14
+ set PATH $GIT_NESTED_ROOT/bin $PATH
15
15
 
16
- # Load git-nested completions
17
- source $GIT_NESTED_ROOT/share/git-nested.fish
16
+ # Tab completion. The script is generated by git-nested itself, from the
17
+ # same tables the argument parser is built from, so it can never drift.
18
+ git-nested completion fish | source
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something git-nested does that it should not
4
+ labels: bug
5
+ ---
6
+
7
+ ## What happened
8
+
9
+ <!-- Paste the command and its output. `-v` or `-d` output helps a lot. -->
10
+
11
+ ## What you expected instead
12
+
13
+ ## How to reproduce
14
+
15
+ <!-- The shortest sequence of commands that shows it, starting from an empty
16
+ repository if you can. -->
17
+
18
+ ## Versions
19
+
20
+ - git-nested: <!-- git nested --version -->
21
+ - git: <!-- git --version -->
22
+ - OS:
23
+ - Installed via: <!-- pip / standalone binary / source .rc -->
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Question or idea
4
+ url: https://github.com/thorsten-klein/git-nested/discussions
5
+ about: For anything that is not a bug report or a concrete feature request.
6
+ - name: Security vulnerability
7
+ url: https://github.com/thorsten-klein/git-nested/security/advisories/new
8
+ about: Report privately here, not as a public issue.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: Feature request
3
+ about: Something git-nested cannot do yet
4
+ labels: enhancement
5
+ ---
6
+
7
+ ## What you are trying to do
8
+
9
+ <!-- The workflow, not the feature. It often turns out there is already a way. -->
10
+
11
+ ## What you would like git-nested to do
12
+
13
+ <!-- If you have a command line in mind, write it out. -->
14
+
15
+ ## What you do instead today
@@ -0,0 +1,17 @@
1
+ ## What this changes
2
+
3
+ <!-- One or two sentences. What is different after this is merged? -->
4
+
5
+ ## Why
6
+
7
+ <!-- The problem it solves. Link the issue if there is one: Fixes #123 -->
8
+
9
+ ## Checklist
10
+
11
+ - [ ] `uv run poe all` passes (ruff, mypy, pyright, bandit, complexipy, and the
12
+ tests at 100% coverage)
13
+ - [ ] New behaviour has a test
14
+ - [ ] README / `--help` / the man page updated, if the change is visible to users
15
+ - [ ] The PR title reads as a [Conventional Commit](https://www.conventionalcommits.org)
16
+ (`feat:`, `fix:`, `docs:`, …) — it becomes the squash-merge commit message
17
+ and the changelog line
@@ -0,0 +1,23 @@
1
+ # Dependabot keeps the two things this repository pins up to date: the actions
2
+ # every workflow runs, and the Python dependencies uv.lock records.
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ groups:
10
+ # One pull request for the lot. Action bumps are almost always routine,
11
+ # and reviewing them together is quicker than reviewing them one by one.
12
+ actions:
13
+ patterns: ['*']
14
+
15
+ - package-ecosystem: uv
16
+ directory: /
17
+ schedule:
18
+ interval: weekly
19
+ groups:
20
+ # Dev tooling moves often and breaks nothing users see, so it travels
21
+ # together; a runtime dependency gets its own pull request.
22
+ dev:
23
+ dependency-type: development
@@ -0,0 +1,22 @@
1
+ # Categories for the notes GitHub generates from merged pull requests. The
2
+ # repository squash-merges with Conventional Commit titles, so the labels
3
+ # below are what sorts a PR into a section -- keep them on the PR, not on the
4
+ # commit.
5
+ changelog:
6
+ exclude:
7
+ labels: [skip-changelog]
8
+ categories:
9
+ - title: Breaking changes
10
+ labels: [breaking]
11
+ - title: Features
12
+ labels: [enhancement, feature]
13
+ - title: Fixes
14
+ labels: [bug, fix]
15
+ - title: Documentation
16
+ labels: [documentation]
17
+ - title: Build, CI and tooling
18
+ labels: [build, ci, tooling]
19
+ - title: Dependencies
20
+ labels: [dependencies]
21
+ - title: Other changes
22
+ labels: ['*']
@@ -0,0 +1,137 @@
1
+ name: Build binary
2
+
3
+ # Builds the standalone 'git-nested' executable for one target and uploads it
4
+ # as an artifact. Called by test.yml (so every push/PR proves the recipe still
5
+ # builds) and by release-binary.yml (which attaches it to the release) -- so
6
+ # what CI exercises is built by exactly the recipe that ships, rather than a
7
+ # lookalike that can drift away from it.
8
+ #
9
+ # One call builds one target: PyInstaller freezes for the machine it runs on
10
+ # and nothing else, so Linux, macOS and Windows binaries come from three calls
11
+ # with different 'runs-on'/'container', not from one job.
12
+ on:
13
+ workflow_call:
14
+ inputs:
15
+ ref:
16
+ description: 'Commit/tag/branch to build. Empty: whatever triggered the caller.'
17
+ type: string
18
+ required: false
19
+ default: ''
20
+ artifact:
21
+ description: 'Name of the uploaded artifact'
22
+ type: string
23
+ required: false
24
+ default: git-nested-binary
25
+ runs-on:
26
+ description: 'Runner to build on -- ubuntu-latest, macos-latest, windows-latest'
27
+ type: string
28
+ required: false
29
+ default: ubuntu-latest
30
+ container:
31
+ description: 'Container to build inside (Linux only). Empty: build on the runner itself.'
32
+ type: string
33
+ required: false
34
+ default: almalinux:8
35
+ python-version:
36
+ description: 'Python to freeze, e.g. 3.12'
37
+ type: string
38
+ required: false
39
+ default: '3.12'
40
+ outputs:
41
+ archive:
42
+ description: 'File name of the built archive, e.g. git-nested-1.2.3-x64-linux.tar.xz. Only meaningful for a single (non-matrix) call.'
43
+ value: ${{ jobs.build.outputs.archive }}
44
+ version:
45
+ description: 'Version of the built git-nested, e.g. 1.2.3'
46
+ value: ${{ jobs.build.outputs.version }}
47
+
48
+ jobs:
49
+ build:
50
+ runs-on: ${{ inputs.runs-on }}
51
+ # An empty string here means "no container", which is what the macOS and
52
+ # Windows callers pass -- containers are a Linux-runner feature.
53
+ #
54
+ # For Linux the default is almalinux:8, and that is a portability
55
+ # decision, not a convenience one: a PyInstaller binary links the glibc of
56
+ # the machine that built it, and glibc is only backward compatible, so
57
+ # built on ubuntu-latest it would refuse to start on anything older than
58
+ # that runner. AlmaLinux 8 has glibc 2.28, the oldest that still runs the
59
+ # node20 the actions below need, which covers every distro from Ubuntu
60
+ # 20.04 / Debian 10 / Fedora 29 / RHEL 8 onwards. glibc-based distros only
61
+ # -- musl ones (Alpine) can never run this binary.
62
+ container: ${{ inputs.container }}
63
+ # bash everywhere, including Windows, where the default is pwsh --
64
+ # create-python-exe.sh is one script for all three targets.
65
+ defaults:
66
+ run:
67
+ shell: bash
68
+ outputs:
69
+ archive: ${{ steps.build.outputs.archive }}
70
+ version: ${{ steps.build.outputs.version }}
71
+ steps:
72
+ # Before the checkout -- without git, actions/checkout silently falls
73
+ # back to downloading a tarball, and a checkout with no .git has no
74
+ # tags for setuptools-scm to derive the version from. Only the container
75
+ # needs this; a hosted runner ships all of it.
76
+ - name: Install build prerequisites
77
+ if: inputs.container != ''
78
+ run: dnf -y install "python${{ inputs.python-version }}" "python${{ inputs.python-version }}-pip" git tar xz
79
+
80
+ # ... and the runners get their python from here instead.
81
+ - uses: actions/setup-python@v7
82
+ if: inputs.container == ''
83
+ with:
84
+ python-version: ${{ inputs.python-version }}
85
+
86
+ - uses: actions/checkout@v7
87
+ with:
88
+ ref: ${{ inputs.ref }}
89
+ # the version frozen into the binary comes from the tags, and the
90
+ # default shallow checkout fetches none of them.
91
+ fetch-depth: 0
92
+
93
+ # the checkout is owned by a different user than this container's root,
94
+ # which git refuses to read from until told it is not a security issue.
95
+ - name: Trust the checkout
96
+ run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
97
+
98
+ - name: Build the executable
99
+ id: build
100
+ run: |
101
+ # In the container python is installed under its versioned name; on
102
+ # a runner setup-python puts a plain 'python' on PATH.
103
+ if [ -n "${{ inputs.container }}" ]; then
104
+ PY="python${{ inputs.python-version }}"
105
+ else
106
+ PY=python
107
+ fi
108
+ scripts/create-python-exe.sh --output dist --python "$PY"
109
+ # The archive name carries the version and the target
110
+ # (git-nested-<version>-<arch>-<os>.tar.xz, .zip on Windows), so it
111
+ # cannot be spelled out ahead of time -- it is published as an
112
+ # output for callers that need the exact name.
113
+ #
114
+ # nullglob: only one of the two patterns ever matches (a build
115
+ # produces .tar.xz or .zip, never both) -- without it the unmatched
116
+ # pattern is passed to ls literally, ls exits non-zero trying to
117
+ # read it, and pipefail+errexit abort the step right here with no
118
+ # visible error (ls's message went to /dev/null).
119
+ shopt -s nullglob
120
+ ARCHIVE="$(ls dist/*.tar.xz dist/*.zip 2>/dev/null | head -1)"
121
+ test -n "$ARCHIVE" || { echo "no archive produced" >&2; exit 1; }
122
+ echo "archive=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT"
123
+ # 'git-nested Version: X.Y.Z' -> X.Y.Z
124
+ EXE=dist/git-nested
125
+ [ -f "$EXE" ] || EXE=dist/git-nested.exe
126
+ echo "version=$("$EXE" --version | head -1 | cut -d' ' -f3)" >> "$GITHUB_OUTPUT"
127
+
128
+ # the archive rather than the bare executable. It is what a release
129
+ # actually ships, and on the two platforms where it is a .tar.xz it
130
+ # keeps the exec bit as well, so consumers just untar and run it.
131
+ - uses: actions/upload-artifact@v7
132
+ with:
133
+ name: ${{ inputs.artifact }}
134
+ path: |
135
+ dist/*.tar.xz
136
+ dist/*.zip
137
+ if-no-files-found: error
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - name: Checkout code
12
- uses: actions/checkout@v4
12
+ uses: actions/checkout@v7
13
13
  with:
14
14
  fetch-depth: 0
15
15
  - name: Install uv
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Build package
23
23
  run: uv run poe build
24
24
  - name: Upload dist
25
- uses: actions/upload-artifact@v4
25
+ uses: actions/upload-artifact@v7
26
26
  with:
27
27
  name: dist
28
28
  path: dist/
@@ -37,7 +37,7 @@ jobs:
37
37
  id-token: write
38
38
  steps:
39
39
  - name: Download dist
40
- uses: actions/download-artifact@v4
40
+ uses: actions/download-artifact@v8
41
41
  with:
42
42
  name: dist
43
43
  path: dist/
@@ -1,9 +1,9 @@
1
1
  name: Release binary
2
2
 
3
- # Builds the single-file 'git-nested' executable (see
4
- # scripts/create-python-exe.sh) and attaches
5
- # git-nested-<version>-x64-linux.tar.xz to the release, so git-nested can be
6
- # used on a machine that has no python -- and no pip, no uv -- at all.
3
+ # Builds the single-file 'git-nested' executable for Linux, macOS and Windows
4
+ # (see scripts/create-python-exe.sh) and attaches each archive to the release,
5
+ # so git-nested can be used on a machine that has no python -- and no pip, no
6
+ # uv -- at all.
7
7
  #
8
8
  # Separate from publish.yml on purpose. That one is triggered by the tag push
9
9
  # and uploads to PyPI, while attaching an asset needs the GitHub *release* to
@@ -40,10 +40,27 @@ jobs:
40
40
  # test.yml so both exercise the same executable. 'env.REF' is not available
41
41
  # to a reusable workflow's inputs, so the ref is spelled out again.
42
42
  build:
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ include:
47
+ # Intel macOS is deliberately absent: those runners are gone, and
48
+ # the arm64 binary is what a current Mac needs.
49
+ - target: linux
50
+ runs-on: ubuntu-latest
51
+ container: almalinux:8
52
+ - target: macos
53
+ runs-on: macos-latest
54
+ container: ''
55
+ - target: windows
56
+ runs-on: windows-latest
57
+ container: ''
43
58
  uses: ./.github/workflows/build-binary.yml
44
59
  with:
45
60
  ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}
46
- artifact: git-nested-binary
61
+ artifact: git-nested-binary-${{ matrix.target }}
62
+ runs-on: ${{ matrix.runs-on }}
63
+ container: ${{ matrix.container }}
47
64
 
48
65
  # The whole pytest suite, driven through the binary that is about to be
49
66
  # attached -- the check that it is functionally complete, not just startable.
@@ -52,7 +69,7 @@ jobs:
52
69
  uses: ./.github/workflows/test-binary.yml
53
70
  with:
54
71
  ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}
55
- artifact: git-nested-binary
72
+ artifact: git-nested-binary-linux
56
73
 
57
74
  # Proves the "runs on any x86_64 Linux" claim rather than assuming it. The
58
75
  # binary is dropped into stock distro images that have no python installed,
@@ -81,14 +98,14 @@ jobs:
81
98
  - image: archlinux:latest
82
99
  install: pacman -Sy --noconfirm git
83
100
  steps:
84
- - uses: actions/download-artifact@v4
101
+ - uses: actions/download-artifact@v8
85
102
  with:
86
- name: git-nested-binary
103
+ name: git-nested-binary-linux
87
104
 
88
- # Named git-nested-<version>-x64-linux.tar.xz -- resolved by glob since
89
- # the version isn't known here.
105
+ # Named git-nested-<version>-<arch>-linux.tar.xz -- resolved by glob
106
+ # since the version isn't known here.
90
107
  - name: Unpack
91
- run: tar -xf git-nested-*-x64-linux.tar.xz
108
+ run: tar -xf git-nested-*-linux.tar.xz
92
109
 
93
110
  # git is the one thing that does have to be installed -- git-nested
94
111
  # drives it and refuses to start without it. Python deliberately is not:
@@ -150,17 +167,22 @@ jobs:
150
167
  permissions:
151
168
  contents: write
152
169
  steps:
153
- - uses: actions/download-artifact@v4
170
+ # every target's artifact, unpacked side by side. 'build' is a matrix,
171
+ # and a matrix job's outputs are last-one-wins, so the exact file names
172
+ # are read off disk here rather than composed from an output.
173
+ - uses: actions/download-artifact@v8
154
174
  with:
155
- name: git-nested-binary
175
+ pattern: git-nested-binary-*
176
+ merge-multiple: true
156
177
 
157
178
  # --clobber so re-running against an already-attached release replaces
158
- # the asset instead of failing halfway. The filename is composed from
159
- # 'build's version output -- this is the point that actually names the
160
- # release asset, so it uses the real version rather than a glob.
161
- - name: Attach git-nested-${{ needs.build.outputs.version }}-x64-linux.tar.xz to ${{ env.REF }}
179
+ # the assets instead of failing halfway.
180
+ - name: Attach the binaries to ${{ env.REF }}
162
181
  env:
163
182
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
164
183
  GH_REPO: ${{ github.repository }}
165
- ARCHIVE: git-nested-${{ needs.build.outputs.version }}-x64-linux.tar.xz
166
- run: gh release upload "$REF" "$ARCHIVE" --clobber
184
+ run: |
185
+ mapfile -t assets < <(ls git-nested-*.tar.xz git-nested-*.zip 2>/dev/null)
186
+ test "${#assets[@]}" -gt 0 || { echo "no archives downloaded" >&2; exit 1; }
187
+ printf '%s\n' "${assets[@]}"
188
+ gh release upload "$REF" "${assets[@]}" --clobber
@@ -1,7 +1,7 @@
1
1
  name: Test binary
2
2
 
3
- # Runs the *whole* pytest suite against the standalone executable instead of
4
- # against the git_nested module in the checkout: conftest's GIT_NESTED_EXE
3
+ # Runs the *whole* pytest suite against the standalone Linux executable rather
4
+ # than against the git_nested module in the checkout: conftest's GIT_NESTED_EXE
5
5
  # routes every cmd_git_nested() call through `git nested ...` on PATH, so the
6
6
  # ~130 tests drive the frozen binary as a user would. That is what catches the
7
7
  # failures unique to freezing -- a module PyInstaller did not collect, package
@@ -30,7 +30,7 @@ jobs:
30
30
  test:
31
31
  runs-on: ubuntu-latest
32
32
  steps:
33
- - uses: actions/checkout@v4
33
+ - uses: actions/checkout@v7
34
34
  with:
35
35
  ref: ${{ inputs.ref }}
36
36
  # no fetch-depth: 0 needed -- the version under test is read out of
@@ -56,19 +56,19 @@ jobs:
56
56
  git config --global --add safe.directory "$PWD";
57
57
  git config --global --add safe.directory "$PWD.git";
58
58
 
59
- - uses: actions/download-artifact@v4
59
+ - uses: actions/download-artifact@v8
60
60
  with:
61
61
  name: ${{ inputs.artifact }}
62
62
 
63
63
  # into its own directory: the env fixture puts the binary's directory on
64
64
  # PATH, and the repo root is not a good thing to put there. Named
65
- # git-nested-<version>-x64-linux.tar.xz, so matched by glob. 'git-nested'
65
+ # git-nested-<version>-<arch>-linux.tar.xz, so matched by glob. 'git-nested'
66
66
  # inside is a symlink to the versioned binary, so both land in bin/
67
67
  # together.
68
68
  - name: Unpack the binary
69
69
  run: |
70
70
  mkdir -p bin
71
- tar -C bin -xf git-nested-*-x64-linux.tar.xz
71
+ tar -C bin -xf git-nested-*-linux.tar.xz
72
72
  bin/git-nested --version
73
73
 
74
74
  # no coverage flags: the code being executed lives in another process
@@ -4,26 +4,55 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  pull_request:
7
- branches: ['*']
7
+ branches: ['**']
8
+
9
+ # One run per branch/PR at a time -- a new commit cancels whatever this
10
+ # workflow is still running for the same ref. Not on main: every push there
11
+ # is a merge, and each one should run to completion rather than being
12
+ # canceled by the next.
13
+ concurrency:
14
+ group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
15
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
8
16
 
9
17
  jobs:
10
18
  # Builds the standalone executable with the very script the release uses
11
19
  # (scripts/create-python-exe.sh), so a change that breaks the freeze -- a new
12
- # import, a moved data file -- fails here rather than on release day. The
13
- # script smoke-tests the binary it produces; release-binary.yml additionally
14
- # runs it across distros before attaching it to a release.
20
+ # import, a moved data file -- fails here rather than on release day. All
21
+ # three targets, because a break is usually a break on one of them only. The
22
+ # script smoke-tests each binary it produces; release-binary.yml additionally
23
+ # runs the Linux one across distros before attaching it to a release.
15
24
  binary:
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ include:
29
+ # Intel macOS is deliberately absent: those runners are gone, and
30
+ # the arm64 binary is what a current Mac needs.
31
+ - target: linux
32
+ runs-on: ubuntu-latest
33
+ container: almalinux:8
34
+ - target: macos
35
+ runs-on: macos-latest
36
+ container: ''
37
+ - target: windows
38
+ runs-on: windows-latest
39
+ container: ''
16
40
  uses: ./.github/workflows/build-binary.yml
17
41
  with:
18
- artifact: git-nested-binary-ci
42
+ artifact: git-nested-binary-ci-${{ matrix.target }}
43
+ runs-on: ${{ matrix.runs-on }}
44
+ container: ${{ matrix.container }}
19
45
 
20
- # ... and then runs the full pytest suite through that executable, so a
21
- # binary that builds but does not work still fails the PR.
46
+ # ... and then runs the full pytest suite through the Linux executable, so a
47
+ # binary that builds but does not work still fails the PR. Linux only: the
48
+ # suite is the same suite either way, and what it is checking here is that
49
+ # freezing did not drop anything -- a per-platform answer the smoke test in
50
+ # create-python-exe.sh already gives for the other two.
22
51
  binary-test:
23
52
  needs: binary
24
53
  uses: ./.github/workflows/test-binary.yml
25
54
  with:
26
- artifact: git-nested-binary-ci
55
+ artifact: git-nested-binary-ci-linux
27
56
 
28
57
  test:
29
58
  runs-on: ${{ matrix.os }}
@@ -34,7 +63,7 @@ jobs:
34
63
 
35
64
  steps:
36
65
  - name: Checkout code
37
- uses: actions/checkout@v4
66
+ uses: actions/checkout@v7
38
67
  with:
39
68
  # SonarCloud's blame-based analysis (new-code detection, issue
40
69
  # authorship) needs real history, not the default shallow, tagless
@@ -58,7 +87,7 @@ jobs:
58
87
  run: uv run poe all
59
88
  - name: Upload coverage reports to Codecov
60
89
  if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
61
- uses: codecov/codecov-action@v5
90
+ uses: codecov/codecov-action@v7
62
91
  with:
63
92
  token: ${{ secrets.CODECOV_TOKEN }}
64
93
  - name: Upload test results to Codecov
@@ -68,6 +97,6 @@ jobs:
68
97
  token: ${{ secrets.CODECOV_TOKEN }}
69
98
  - name: SonarCloud Scan
70
99
  if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
71
- uses: SonarSource/sonarqube-scan-action@v4
100
+ uses: SonarSource/sonarqube-scan-action@v8
72
101
  env:
73
102
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -1,4 +1,4 @@
1
- /test/tmp/
1
+ /tests/tmp/
2
2
  __pycache__
3
3
  .venv/
4
4
  *.pyc
@@ -33,14 +33,15 @@ repos:
33
33
  - id: debug-statements
34
34
  - id: name-tests-test
35
35
  args: ['--pytest-test-first']
36
+ # tests/unit/fakes.py holds the shared test doubles. It cannot be a
37
+ # conftest.py -- pytest registers every conftest under the bare name
38
+ # 'conftest', so a second one would collide in sys.modules with
39
+ # tests/conftest.py, which the whole e2e suite imports by that name.
40
+ exclude: ^tests/unit/fakes\.py$
36
41
 
37
42
  # --- scripts --------------------------------------------------------
38
43
  - id: check-executables-have-shebangs
39
44
  - id: check-shebang-scripts-are-executable
40
- # share/*.bash and share/*.sh are meant to be *sourced* into a user's
41
- # shell (see share/enable-completion.sh), never executed directly --
42
- # their shebang is there only so editors pick the right syntax mode.
43
- exclude: ^share/
44
45
 
45
46
  # --- repo integrity -------------------------------------------------
46
47
  - id: detect-private-key
@@ -13,7 +13,13 @@ GIT_NESTED_ROOT=$(
13
13
  ) || return
14
14
 
15
15
  export GIT_NESTED_ROOT
16
- export PATH=$GIT_NESTED_ROOT/lib:$PATH
16
+ export PATH=$GIT_NESTED_ROOT/bin:$PATH
17
17
  export MANPATH=$GIT_NESTED_ROOT/man:$MANPATH
18
18
 
19
- source "$GIT_NESTED_ROOT/share/enable-completion.sh"
19
+ # Tab completion. The script is generated by git-nested itself, from the
20
+ # same tables the argument parser is built from, so it can never drift.
21
+ if [[ -n ${ZSH_VERSION-} ]]; then
22
+ eval "$(git-nested completion zsh)"
23
+ else
24
+ eval "$(git-nested completion bash)"
25
+ fi
@@ -0,0 +1,95 @@
1
+ # Contributing to git-nested
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ git clone https://github.com/YOUR_USERNAME/git-nested
7
+ cd git-nested
8
+ uv sync --dev # or: pip install -e ".[dev]"
9
+ uv run poe all # verify your setup: the full gate
10
+ ```
11
+
12
+ ## Making changes
13
+
14
+ 1. Branch off `main`.
15
+ 2. Make your change and add tests for it.
16
+ 3. Run `uv run poe all` before opening a PR.
17
+ 4. Open a PR against `main` and fill out the template.
18
+
19
+ ## How the tests are laid out
20
+
21
+ ```
22
+ tests/
23
+ conftest.py shared fixtures and the git-nested runner
24
+ unit/ fast, no repositories -- one function at a time, with fakes
25
+ e2e/ real git repositories in a temp dir, driven through the CLI
26
+ ```
27
+
28
+ Each directory is also a marker (`-m unit`, `-m e2e`), applied by path, so a
29
+ new test file needs nothing added to it.
30
+
31
+ ```bash
32
+ uv run poe test # everything, with the coverage gate
33
+ uv run poe test-unit # just the unit tests -- seconds, no coverage gate
34
+ uv run poe test tests/e2e/test_clone.py # one file
35
+ uv run pytest -k clone # or pytest directly
36
+ ```
37
+
38
+ `poe test` requires **100% line coverage** and fails below it. A line that
39
+ genuinely cannot be reached gets a `# pragma: no cover` with a comment saying
40
+ why.
41
+
42
+ Tests run in parallel against real repositories, so a flake is possible. To
43
+ hunt one down:
44
+
45
+ ```bash
46
+ uv run poe flakefinder # reruns the suite 200 times; add -k to narrow it
47
+ ```
48
+
49
+ ### The standalone executable
50
+
51
+ Releases ship a single-file executable built with PyInstaller
52
+ (`scripts/create-python-exe.sh`). Freezing can break things the module tests
53
+ never see, so the whole suite can also run *through* the binary:
54
+
55
+ ```bash
56
+ uv run poe test-exe # build dist/git-nested and test it
57
+ uv run poe exe # just build it
58
+ GIT_NESTED_EXE=dist/git-nested uv run pytest # test an existing binary
59
+ ```
60
+
61
+ CI does both: `test.yml` builds the binary and tests it on every PR;
62
+ `release-binary.yml` repeats that across six distro images before attaching
63
+ the asset to a release.
64
+
65
+ ## Code quality
66
+
67
+ `uv run poe all` runs the full gate CI enforces. Each check has its own task:
68
+
69
+ ```bash
70
+ uv run poe pre-commit # repo hygiene: whitespace, YAML/TOML/JSON, shebangs
71
+ uv run poe lint # ruff lint
72
+ uv run poe format # ruff format
73
+ uv run poe mypy # static typing
74
+ uv run poe pyright # static typing (different checker, both must pass)
75
+ uv run poe bandit # security linting
76
+ uv run poe pip-audit # dependency vulnerability scan
77
+ uv run poe complexity # cognitive complexity (complexipy)
78
+ ```
79
+
80
+ ## Commit messages
81
+
82
+ Imperative mood ("Add", not "Added"), first line under 72 characters,
83
+ reference issues where relevant.
84
+
85
+ ## Documentation
86
+
87
+ Update [README.md](README.md) for user-facing changes and
88
+ [docs/diagrams.md](docs/diagrams.md) when a command's underlying git calls
89
+ change.
90
+
91
+ ## Reporting bugs and security issues
92
+
93
+ Open a GitHub issue with steps to reproduce, expected vs. actual behaviour,
94
+ and your git-nested/git/Python versions. For security vulnerabilities, see
95
+ [SECURITY.md](SECURITY.md) instead of opening a public issue.