git-p4son 0.7.0__tar.gz → 0.9.0__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 (90) hide show
  1. {git_p4son-0.7.0 → git_p4son-0.9.0}/PKG-INFO +507 -409
  2. {git_p4son-0.7.0 → git_p4son-0.9.0}/README.md +505 -407
  3. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/__init__.py +1 -1
  4. git_p4son-0.9.0/git_p4son/alias.py +186 -0
  5. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/backend.py +22 -3
  6. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/changelist_store.py +85 -17
  7. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/cli.py +60 -19
  8. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/common.py +17 -0
  9. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/complete.py +23 -3
  10. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/git.py +4 -2
  11. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/git_backend.py +3 -0
  12. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/hooks.py +3 -54
  13. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/included.py +26 -7
  14. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/jj.py +26 -5
  15. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/jj_backend.py +15 -6
  16. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/lib.py +128 -70
  17. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/log.py +6 -1
  18. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/new.py +48 -11
  19. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/per_commit.py +1 -3
  20. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/perforce.py +111 -17
  21. git_p4son-0.9.0/git_p4son/review_tools.py +168 -0
  22. git_p4son-0.9.0/git_p4son/scripts.py +64 -0
  23. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/skill/git-p4son.md +27 -12
  24. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/sync.py +2 -0
  25. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/update.py +34 -12
  26. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son.egg-info/PKG-INFO +507 -409
  27. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son.egg-info/SOURCES.txt +5 -0
  28. {git_p4son-0.7.0 → git_p4son-0.9.0}/pyproject.toml +2 -2
  29. git_p4son-0.9.0/tests/test_alias.py +364 -0
  30. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_changelist_store.py +122 -2
  31. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_common.py +22 -0
  32. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_complete.py +26 -1
  33. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_git.py +1 -1
  34. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_hooks.py +9 -76
  35. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_jj.py +219 -4
  36. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_lib.py +69 -104
  37. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_log.py +16 -0
  38. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_new.py +270 -4
  39. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_perforce.py +78 -1
  40. git_p4son-0.9.0/tests/test_renames.py +144 -0
  41. git_p4son-0.9.0/tests/test_review_tools.py +216 -0
  42. git_p4son-0.9.0/tests/test_scripts.py +89 -0
  43. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_sync.py +3 -2
  44. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_update.py +121 -5
  45. git_p4son-0.7.0/git_p4son/alias.py +0 -147
  46. git_p4son-0.7.0/tests/test_alias.py +0 -237
  47. {git_p4son-0.7.0 → git_p4son-0.9.0}/LICENSE +0 -0
  48. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/__main__.py +0 -0
  49. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/_git-p4son +0 -0
  50. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/git-p4son-jj.bash +0 -0
  51. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/git-p4son-jj.ps1 +0 -0
  52. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/git-p4son-jj.zsh +0 -0
  53. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/git-p4son.bash +0 -0
  54. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/completions/git-p4son.ps1 +0 -0
  55. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/config.py +0 -0
  56. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/depot.py +0 -0
  57. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/description.py +0 -0
  58. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/init.py +0 -0
  59. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/jj_lock.py +0 -0
  60. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/list_changes.py +0 -0
  61. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/processes.py +0 -0
  62. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/skill/SKILL.md +0 -0
  63. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/skill.py +0 -0
  64. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/sync_branch.py +0 -0
  65. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/sync_merge.py +0 -0
  66. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/sync_split_users.py +0 -0
  67. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/toml_writer.py +0 -0
  68. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/words.py +0 -0
  69. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son/writable.py +0 -0
  70. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son.egg-info/dependency_links.txt +0 -0
  71. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son.egg-info/entry_points.txt +0 -0
  72. {git_p4son-0.7.0 → git_p4son-0.9.0}/git_p4son.egg-info/top_level.txt +0 -0
  73. {git_p4son-0.7.0 → git_p4son-0.9.0}/setup.cfg +0 -0
  74. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_catch_up.py +0 -0
  75. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_cli.py +0 -0
  76. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_completion_scripts.py +0 -0
  77. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_config.py +0 -0
  78. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_depot.py +0 -0
  79. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_description.py +0 -0
  80. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_git_backend.py +0 -0
  81. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_init.py +0 -0
  82. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_isolation.py +0 -0
  83. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_list_changes.py +0 -0
  84. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_per_commit.py +0 -0
  85. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_processes.py +0 -0
  86. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_skill.py +0 -0
  87. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_sync_merge.py +0 -0
  88. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_sync_split_users.py +0 -0
  89. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_toml_writer.py +0 -0
  90. {git_p4son-0.7.0 → git_p4son-0.9.0}/tests/test_writable.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Utility for keeping a Perforce workspace and local git or jj repo in sync
5
5
  Author-email: Andreas Andersson <andreas@neoboid.com>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/neoboid/git-p4son
8
8
  Project-URL: Repository, https://github.com/neoboid/git-p4son
9
9
  Project-URL: Issues, https://github.com/neoboid/git-p4son/issues
10
10
  Project-URL: Changelog, https://github.com/neoboid/git-p4son/blob/main/CHANGELOG.md
11
- Keywords: perforce,git,version-control,sync
11
+ Keywords: perforce,git,jj,jujutsu,version-control,sync
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Programming Language :: Python :: 3
@@ -25,31 +25,22 @@ Dynamic: license-file
25
25
 
26
26
  `git-p4son` lets you work in a Perforce workspace with a local git repository, or a colocated
27
27
  [Jujutsu (jj)](#jujutsu-jj) one, so you can use the familiar local development flow even though your project uses
28
- Perforce for source control.
28
+ Perforce.
29
29
 
30
- A `main` branch (a bookmark in jj) is kept in sync with your Perforce workspace, one commit per changelist. You work
31
- on feature branches or jj changes and rebase them onto `main` after a sync; with jj, `sync` rebases the change you
32
- sync from for you.
30
+ The core idea is a local `main` branch kept in sync with the Perforce workspace. Each sync commits the changes since
31
+ the last one. You work on feature branches based on `main`, and rebase them onto it after a sync.
33
32
 
34
- When your changes are ready, `git-p4son` creates the changelist and shelves it for a Swarm review. It can post each
35
- commit as a patch of its own, so reviewers see the changes in the order you made them, much like a pull request.
36
- Updates fold in only the new commits.
37
-
38
- With jj, `git-p4son` works on the jj change: the changelist's alias follows the change through rewrites, the message
39
- comes from its description, and `jj p4son` runs every command.
40
-
41
- ## On big workspaces
42
- Sometimes git struggles to keep entire perforce workspaces in the repo, like when working
43
- on a Unreal Engine game with all source code in the workspace. If so, it helps to edit
44
- `.gitignore` and include only the subdirectories in which you work, the rest of the files
45
- will still be managed by perforce of course.
33
+ When your feature is ready, `git-p4son` creates a changelist from the commits on your branch. It can also create a
34
+ review, in Swarm or another review tool, with each commit as its own patch, so reviewers see the changes in the order
35
+ you made them, like the commits of a pull request on GitHub. To address feedback, commit more locally, and
36
+ `git-p4son` adds the new commits to the changelist and the review.
46
37
 
47
38
  ## Usage Example
48
39
 
49
40
  Here's a typical workflow when developing a feature using `git-p4son`:
50
41
 
51
42
  ```sh
52
- # Sync main with latest changes from perforce
43
+ # Sync main with the latest changes from Perforce
53
44
  git checkout main
54
45
  git p4son sync
55
46
 
@@ -60,11 +51,9 @@ git checkout -b my-fancy-feature
60
51
  git add .
61
52
  git commit -m "Feature part1"
62
53
 
63
- # Sync to the latest changelist affecting the workspace
54
+ # Sync again, and rebase your branch onto main
64
55
  git checkout main
65
56
  git p4son sync
66
-
67
- # Rebase your changes on main
68
57
  git checkout my-fancy-feature
69
58
  git rebase main
70
59
 
@@ -72,32 +61,19 @@ git rebase main
72
61
  git add .
73
62
  git commit -m "Feature part2"
74
63
 
75
- # Create a Swarm review with all commits since main in one go.
76
- # Each commit is checked out in turn and gets a shelf of its own in the
77
- # review, then you are back on your branch.
78
- # An alias called "my-fancy-feature" (derived from the branch name) is
79
- # saved so you can refer to this CL by name in follow-up commands.
64
+ # Create a review with a patch per commit, under an alias named after the branch
80
65
  git p4son review -m "My fancy feature" -b main
81
66
 
82
- # After review feedback, make more changes
67
+ # Address review feedback, then add the new commit to the changelist and the review
83
68
  git add .
84
69
  git commit -m "Address review feedback"
85
-
86
- # Update the changelist with latest commit, re-open files, and re-shelve.
87
- # The changelist is looked up by branch name automatically.
88
70
  git p4son update --shelve
89
71
 
90
- # After approval, submit in p4v
91
-
92
- # Sync to the latest changelist from perforce
72
+ # After approval, submit in P4V, then sync and clean up
93
73
  git checkout main
94
74
  git p4son sync
95
-
96
- # Force remove old branch as you don't need it anymore
97
- git branch -D my-fancy-feature
98
-
99
- # Remove changelist alias for the branch
100
- git p4son alias delete my-fancy-feature
75
+ git branch -D my-fancy-feature # -D, as the submitted change has other commit hashes
76
+ git p4son alias clean # deletes the aliases of submitted changelists
101
77
 
102
78
  # Start working on the next feature
103
79
  git checkout -b my-next-fancy-feature
@@ -119,8 +95,7 @@ jj p4son sync
119
95
  jj new -m "Feature part2"
120
96
  jj bookmark create my-fancy-feature
121
97
 
122
- # Create a Swarm review with every change since main, one shelf each.
123
- # The alias is named after the bookmark and records the jj change.
98
+ # Create a review with a patch per change, under an alias named after the bookmark
124
99
  jj p4son review -m "My fancy feature" -b main
125
100
 
126
101
  # After review feedback, add a change and move the bookmark to it
@@ -128,11 +103,11 @@ jj new -m "Address review feedback"
128
103
  jj bookmark set my-fancy-feature -r @
129
104
  jj p4son update --shelve
130
105
 
131
- # After approval, submit in p4v, then abandon the submitted changes before
132
- # syncing, which deletes the bookmark too
106
+ # After approval, submit in P4V, abandon the submitted changes, which deletes the
107
+ # bookmark too, then sync and clean up
133
108
  jj abandon main..my-fancy-feature
134
- jj p4son alias delete my-fancy-feature
135
109
  jj p4son sync
110
+ jj p4son alias clean
136
111
 
137
112
  # Start working on the next feature
138
113
  jj new main -m "Next fancy feature"
@@ -141,13 +116,30 @@ jj new main -m "Next fancy feature"
141
116
  `sync` only rebases the change you sync from, and the stack above it. Bring other bookmarks up to date with
142
117
  `jj rebase -b <bookmark> -d main`.
143
118
 
119
+ ## Contents
120
+
121
+ - [Installation](#installation)
122
+ - [Setup](#setup)
123
+ - [Commands](#commands): [init](#init-command), [sync](#sync-command), [sync-split-users](#sync-split-users-command),
124
+ [new](#new-command), [update](#update-command), [review](#review-command), [list-changes](#list-changes-command),
125
+ [alias](#alias-command), [writable](#writable-command), [completion](#completion-command), [skill](#skill-command)
126
+ - [Configuration](#configuration)
127
+ - [Review Tools](#review-tools)
128
+ - [Hooks](#hooks)
129
+ - [Jujutsu (jj)](#jujutsu-jj)
130
+ - [Claude Code Skill](#claude-code-skill)
131
+ - [Shell Completions](#shell-completions)
132
+ - [Development](#development)
133
+
144
134
  ## Installation
145
135
 
146
136
  ```sh
147
137
  pip install git-p4son
148
138
  ```
149
139
 
150
- Requires Python 3.11 or later.
140
+ Requires Python 3.11 or later. For tab completion in bash, zsh and PowerShell, see
141
+ [Shell Completions](#shell-completions). To let Claude Code drive git-p4son, see
142
+ [Claude Code Skill](#claude-code-skill).
151
143
 
152
144
  ### Updating
153
145
 
@@ -155,45 +147,6 @@ Requires Python 3.11 or later.
155
147
  pip install --upgrade git-p4son
156
148
  ```
157
149
 
158
- For tab completion (zsh and PowerShell), see [Shell Completions](#shell-completions). To let Claude Code drive
159
- git-p4son, see [Claude Code Skill](#claude-code-skill).
160
-
161
- ## Development
162
-
163
- To install from source in editable mode:
164
-
165
- ```sh
166
- git clone https://github.com/neoboid/git-p4son.git
167
- cd git-p4son
168
- pip install -e .
169
- ```
170
-
171
- The `-e` flag installs the package in "editable" mode, meaning changes to the code are immediately available
172
- without reinstalling. This is also handy if you want to auto-update git-p4son whenever you pull from GitHub.
173
-
174
- git-p4son only uses Python standard library modules - no additional packages are required.
175
-
176
- Run the tests with `python -m pytest tests/` (`pip install pytest`). With `pytest-xdist` installed, add
177
- `-n auto --maxprocesses 8` to run them in parallel, which is about three times faster.
178
-
179
- ### Git Hooks
180
-
181
- The repository includes git hooks in the `hooks/` directory:
182
-
183
- - **pre-commit** - verifies that staged Python files are formatted with `autopep8`. Install it with
184
- `pip install autopep8`.
185
- - **pre-push** - runs the test suite and blocks the push if any test fails. It needs `pytest` installed, and runs
186
- the tests in parallel when `pytest-xdist` is too.
187
-
188
- To activate the hooks, run:
189
-
190
- ```sh
191
- scripts/setup-hooks.sh
192
- ```
193
-
194
- This configures `core.hooksPath` so git uses the hooks from the repository. You only need to run this once after
195
- cloning.
196
-
197
150
  ## Setup
198
151
 
199
152
  These steps set up git-p4son in an existing Perforce workspace. You only need to do this once.
@@ -210,13 +163,13 @@ These steps set up git-p4son in an existing Perforce workspace. You only need to
210
163
  cd /path/to/your/workspace # or a subdirectory of it
211
164
  git p4son init
212
165
  ```
213
- The command verifies that you are inside a P4 workspace, prompts you to select a depot
214
- root (entire workspace or current directory subtree), runs `git init`, sets up `.gitignore` (copying from
215
- `.p4ignore` if available), and creates an initial commit.
216
- With jj 0.41 or newer installed, it also offers to colocate jj and to add the `jj p4son` alias, see
217
- [Jujutsu (jj)](#jujutsu-jj).
166
+ Answer its questions, starting with the depot root: the whole workspace or the subtree you're in. It then runs
167
+ `git init`, sets up `.gitignore` and makes a first commit holding only that. See [Init Command](#init-command) for
168
+ what it asks.
218
169
 
219
- 3. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded.
170
+ 3. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded. In a big
171
+ workspace, such as an Unreal Engine game with all of the engine source in it, git can struggle to track
172
+ everything. Ignore all but the directories you work in; Perforce still manages the rest.
220
173
 
221
174
  4. **Add and commit all files manually the first time**
222
175
  ```sh
@@ -233,94 +186,23 @@ These steps set up git-p4son in an existing Perforce workspace. You only need to
233
186
  From here, branch off `main` for local development. See the [Usage Example](#usage-example) for a typical
234
187
  workflow.
235
188
 
236
- ### How sync handles writable files
237
-
238
- When you switch branches with `git checkout`, git removes the read-only flag on every file it writes. This means
239
- that when you switch back to `main` for a sync, Perforce may refuse to overwrite those files because they are
240
- writable. git-p4son handles these automatically by classifying each writable file:
241
-
242
- **writable-unchanged** - The file on disk is identical to the last sync (only the read-only flag was removed
243
- by git). This is the common case after switching branches. git-p4son detects this by comparing git blob IDs against
244
- the last sync commit that touched the file, restores the read-only flag, and lets the sync overwrite the file.
245
-
246
- **writable-changed** - The file on disk differs from the last sync. This happens when you manually edit a file
247
- on `main` and commit it to git without submitting to Perforce. git-p4son lets the sync overwrite the file and
248
- includes it in the sync commit (so the commit reflects pure Perforce state), then three-way merges your local
249
- changes on top of the new Perforce content - similar to a `git rebase`. If the merge is clean, the result appears
250
- as an unstaged change. If there are conflicts, standard conflict markers are inserted for you to resolve.
251
-
252
- **added both locally and in Perforce** - A file you committed to git is also being added by Perforce (p4 has never
253
- synced it to your workspace). There is no common baseline, so git-p4son merges the two versions against an empty
254
- base: identical content merges cleanly, otherwise the file gets a single conflict block containing both full
255
- versions for you to resolve.
256
-
257
- **binary files** - Binary files that have local changes cannot be three-way merged. git-p4son restores your local
258
- version to disk after the sync commit so it shows up in `git status` for you to handle manually.
259
-
260
- **added locally, deleted upstream** - A file with local changes in git was deleted in Perforce. git-p4son lets the
261
- delete stand and warns you - your version remains available in git history.
262
-
263
- **deleted locally, added upstream** - A file you deleted from git was modified in Perforce. The Perforce version is
264
- kept in the sync commit. You can delete it again if you want.
265
-
266
- **ignored** - Files ignored by `.gitignore` are skipped with a warning. These are outside git's domain and should
267
- be managed manually or via Perforce directly.
268
-
269
- After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
270
- commit any files that need attention.
271
-
272
- **Workspaces with `allwrite`** - When your client spec has the `allwrite` option, every file is writable by design,
273
- so the read-only flag carries no information about local changes. git-p4son classifies files exactly as above (the
274
- verdict comes from git, not from the flag), but leaves the write bit alone on files it finds unchanged: Perforce
275
- compares digests before refusing to overwrite a writable file, and unchanged files pass that check. Only files with
276
- local changes are made read-only for the duration of the sync, since those are the ones Perforce would otherwise
277
- refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync normally, and after the
278
- sync git-p4son lists the ones Perforce skipped because they were modified locally.
279
-
280
- **Writable mode** - With [writable mode](#writable-command) on, the files a sync touches are made writable again after
281
- the sync if git tracks them. The sync itself works as described above, since the workspace is not `allwrite`.
282
-
283
- ## Configuration
284
-
285
- git-p4son keeps its per-repository settings in `.git-p4son/config.toml`. `init`, `writable` and `sync-split-users`
286
- write most of them; the rest you edit by hand.
287
-
288
- - `[depot] root` (string, set by `init`): the depot path `sync` syncs, e.g. `//$(workspace)` or
289
- `//$(workspace)/Engine`. See [Init Command](#init-command)
290
- - `[core] writable` (`true` or `false`, default `false`): [writable mode](#writable-command), which keeps
291
- git-tracked files writable
292
- - `[core] backend` (`"auto"`, `"git"` or `"jj"`, default `"auto"`): the tool git-p4son follows for the change, the
293
- base and the branch. `auto` means jj in a [jj workspace](#jujutsu-jj) and git otherwise
294
- - `[sync] branch` (string, set by `init`): the sync branch, which `sync` commits onto from whatever branch you are
295
- on. Without it, `sync` uses `main`. See [Sync Command](#sync-command)
296
- - `[sync] split-users` (list of strings, default empty): the users whose changelists `sync` gives a commit each;
297
- `$(user)` stands for you. See [Split users](#split-users)
298
- - `[sync] blocking-processes` (list of strings, default empty): processes that stop a sync while they run. See
299
- [Blocking processes](#blocking-processes)
300
- - `[hooks.extension-associations]` (table, Windows only): the command that runs a hook script, by file extension.
301
- See [Hooks](#hooks)
302
-
303
- For example:
304
-
305
- ```toml
306
- [depot]
307
- root = "//$(workspace)"
308
-
309
- [core]
310
- writable = true
311
-
312
- [sync]
313
- split-users = ["$(user)", "alice"]
314
- blocking-processes = ["UnrealEditor"]
315
- ```
316
-
317
- A value of the wrong type, such as `writable = "true"` or `split-users = "alice"`, or a file that is not valid TOML,
318
- stops the command with an error naming the setting, so a typo is never silently ignored.
189
+ ## Commands
319
190
 
320
- ## Usage
191
+ git-p4son provides eleven commands:
321
192
 
322
- git-p4son provides eleven commands: `init`, `sync`, `sync-split-users`, `new`, `update`, `review`,
323
- `list-changes`, `alias`, `writable`, `completion`, and `skill`.
193
+ | Command | What it does |
194
+ | --- | --- |
195
+ | [`init`](#init-command) | Set up a git repository in a Perforce workspace |
196
+ | [`sync`](#sync-command) | Sync the workspace from Perforce and commit the changes to `main` |
197
+ | [`sync-split-users`](#sync-split-users-command) | Show or edit the users whose changelists `sync` commits one by one |
198
+ | [`new`](#new-command) | Create a changelist from the commits on your branch, optionally with a review |
199
+ | [`update`](#update-command) | Add the newer commits to an existing changelist |
200
+ | [`review`](#review-command) | Create a review with one shelf per commit |
201
+ | [`list-changes`](#list-changes-command) | List the commit subjects since a base branch |
202
+ | [`alias`](#alias-command) | Manage the names that stand for changelist numbers |
203
+ | [`writable`](#writable-command) | Keep git-tracked files writable, without `p4 edit` first |
204
+ | [`completion`](#completion-command) | Print the path to a shell completion script |
205
+ | [`skill`](#skill-command) | Install or show the Claude Code skill |
324
206
 
325
207
  To see help for any command, use `-h`:
326
208
 
@@ -339,48 +221,37 @@ Alternatively, call the executable directly: `git-p4son --help`.
339
221
 
340
222
  ### Init Command
341
223
 
342
- Initialize a git repository inside a Perforce workspace:
224
+ Set up git-p4son in a Perforce workspace, from anywhere inside it:
343
225
 
344
226
  ```sh
345
227
  git p4son init
346
228
  ```
347
229
 
348
- This command checks that it runs in a Perforce workspace, then asks for the depot root, writable mode, split users,
349
- the Claude Code skill and jj. It then sets up `.gitignore`, runs `git init` and creates a first commit holding only
350
- `.gitignore`. It ends by listing the next steps.
351
-
352
- The depot root determines which part of the Perforce workspace git-p4son syncs. You can choose to sync the entire
353
- workspace or just the directory's subtree where the git root is placed. The selection is saved in
354
- `.git-p4son/config.toml` and used by all subsequent commands.
230
+ `init` checks that it runs in a Perforce workspace, then asks:
355
231
 
356
- Nothing in `.git-p4son/` belongs in git. The folder has its own `.gitignore` that ignores everything in it, so there
357
- is no need to list it in the workspace `.gitignore`.
232
+ - **Depot root:** which part of the workspace git-p4son syncs, the whole workspace or the subtree the git root is in.
233
+ It's saved as `[depot] root` in `.git-p4son/config.toml`, with a `$(workspace)` placeholder for the workspace name,
234
+ e.g. `//$(workspace)/Engine`, so renaming the workspace doesn't break it. A concrete name works too.
235
+ - **Writable mode:** whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
236
+ saves the answer; run `git p4son writable apply` once your files are committed.
237
+ - **Split users:** whether to sync your own changelists one by one, as a [split user](#split-users), defaulting to the
238
+ current setting. Yes adds a `$(user)` entry and no removes it; other split users are left alone.
239
+ - **Claude Code skill:** if Claude Code is installed, whether to install the [skill](#claude-code-skill). An installed
240
+ skill is refreshed without asking.
241
+ - **jj:** with jj 0.41 or newer, whether to colocate jj and add the `jj p4son` alias, as described below.
358
242
 
359
- The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
360
- `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
361
- the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
362
- by hand-editing `config.toml`; both forms work.
243
+ Then it sets up the repository:
363
244
 
364
- The `.gitignore` is set up using this priority:
365
- - If `.gitignore` already exists, it is left as is
366
- - If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
367
- - Otherwise, an empty `.gitignore` is created
245
+ - **`.gitignore`:** kept if it exists, else copied from `.p4ignore`, else created empty.
246
+ - **git:** `git init`, and a first commit holding only `.gitignore`.
247
+ - **Sync branch:** recorded as `[sync] branch`. In a repository it creates, that's the branch `HEAD` is on. In an
248
+ existing one, it's `main` if there is one, else the branch `HEAD` is on, else one you pick from the local branches.
249
+ A branch already set is kept.
368
250
 
369
- `init` also asks whether to turn on [writable mode](#writable-command), defaulting to the current setting. It only
370
- saves the answer; run `git p4son writable apply` once your files are committed to update their permissions.
251
+ It ends by listing the next steps. Nothing in `.git-p4son/` belongs in git: the folder has its own `.gitignore` that
252
+ ignores everything in it.
371
253
 
372
- `init` then asks whether to sync your own changelists individually, making you a [split user](#split-users).
373
- The default is the current setting. Yes adds a `$(user)` entry to the split users and no removes it; other split
374
- users are left alone.
375
-
376
- `init` records the [sync branch](#sync-command) as `[sync] branch`. In a repository it creates, that's the branch
377
- `HEAD` is on. In an existing one, it's `main` if there is one, else the branch `HEAD` is on, else one you pick from
378
- the local branches. It keeps a branch already set.
379
-
380
- If Claude Code is installed, `init` offers to install the [Claude Code skill](#claude-code-skill). An
381
- already installed skill is refreshed without asking.
382
-
383
- With jj 0.41 or newer installed, `init` offers two things for [jj](#jujutsu-jj):
254
+ For [jj](#jujutsu-jj), `init` offers:
384
255
 
385
256
  - **Colocating jj**, when the workspace has no `.jj` folder: "Use jj in this workspace too (colocated)?", default
386
257
  no. On yes, in an existing repository it runs `jj git init --colocate` as its last step, so jj imports the branch
@@ -394,34 +265,19 @@ Running `init` again asks neither once they are done.
394
265
 
395
266
  ### Sync Command
396
267
 
397
- Sync local git repository with a Perforce workspace:
268
+ Sync the workspace from Perforce and commit what changed onto the sync branch, `main` by default:
398
269
 
399
270
  ```sh
400
- git p4son sync [changelist ...] [--branch BRANCH] [--force] [--dry-run] [--split-user NAME ...] [--no-split]
401
- [--ignore-blocking-processes]
271
+ git p4son sync [changelist ...] [--branch BRANCH] [--no-rebase] [--force] [--dry-run] [--split-user NAME ...]
272
+ [--no-split] [--ignore-blocking-processes]
402
273
  ```
403
274
 
404
- `sync` commits onto the sync branch, `[sync] branch` in `.git-p4son/config.toml`, which `init` sets and which
405
- defaults to `main`. From another branch, it checks the sync branch out, syncs and commits, then returns you to
406
- where you started, telling you to `git rebase` your branch onto the sync branch; it doesn't rebase for you. The
407
- pre-sync and post-sync hooks run on the sync branch. You stay on the sync branch when:
408
-
409
- - local changes were [merged back](#how-sync-handles-writable-files) into the work tree, for you to review and
410
- commit there
411
- - a sync fails partway, with the files as Perforce left them and the commits so far kept
412
-
413
- A failing pre-sync hook, or anything else that stops `sync` before it syncs, returns you to where you started.
414
-
415
275
  **Arguments:**
416
276
  - `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
417
277
  to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
418
278
  strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
419
279
  changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
420
280
 
421
- `sync` requires clean git and Perforce workspaces: no uncommitted changes, untracked files included, and no
422
- git-tracked files opened in Perforce. It checks them after any [blocking processes](#blocking-processes) and before
423
- the [pre-sync hooks](#pre-sync-hook).
424
-
425
281
  **Options:**
426
282
  - `--branch BRANCH`: Commit onto this branch instead of `[sync] branch`. With the jj backend, the bookmark to move
427
283
  to each sync commit
@@ -452,6 +308,21 @@ git p4son sync --no-split # sync without splitting out the configured split us
452
308
  git p4son sync --branch p4 # commit onto the p4 branch this time
453
309
  ```
454
310
 
311
+ `sync` commits onto the sync branch, `[sync] branch` in `.git-p4son/config.toml`, which `init` sets and which
312
+ defaults to `main`. From another branch, it checks the sync branch out, syncs and commits, then returns you to
313
+ where you started, telling you to `git rebase` your branch onto the sync branch; it doesn't rebase for you. The
314
+ pre-sync and post-sync hooks run on the sync branch. You stay on the sync branch when:
315
+
316
+ - local changes were [merged back](#how-sync-handles-writable-files) into the work tree, for you to review and
317
+ commit there
318
+ - a sync fails partway, with the files as Perforce left them and the commits so far kept
319
+
320
+ A failing pre-sync hook, or anything else that stops `sync` before it syncs, returns you to where you started.
321
+
322
+ `sync` requires clean git and Perforce workspaces: no uncommitted changes, untracked files included, and no
323
+ git-tracked files opened in Perforce. It checks them after any [blocking processes](#blocking-processes) and before
324
+ the [pre-sync hooks](#pre-sync-hook).
325
+
455
326
  #### Blocking processes
456
327
 
457
328
  Syncing while e.g. the Unreal editor is open lets p4 replace assets the editor still has loaded, so the editor
@@ -496,17 +367,61 @@ visits `101 102 103 104 105 106`.
496
367
  Splitting only moves forward from a previous sync. The first sync, `last-synced`, and syncing to an older changelist
497
368
  with `--force` are never split. Without any split users, `sync` makes no extra Perforce queries.
498
369
 
499
- Finding the split users' changelists costs one Perforce query per split user, listing their changelists in the range
500
- being synced, then one batched query that checks each changelist found touched the workspace and finds the one before
501
- it. None of them scans the files of every changelist in the range, which on a large range can exceed the server's
502
- `maxscanrows`. They run after the blocking processes check, the clean-workspace checks and the pre-sync hooks, so a
503
- running blocking process, a dirty workspace or a hook that aborts the sync says so before that work. `--dry-run` skips
504
- the checks and hooks but still shows the resulting sequence.
370
+ Finding the split users' changelists takes one Perforce query per split user and one more, none of which scans every
371
+ file in the range, so a large range stays within the server's `maxscanrows`. They run after the checks and the
372
+ pre-sync hooks, so those fail first. `--dry-run` skips the checks and hooks but still shows the resulting sequence.
505
373
 
506
- #### Hooks
374
+ #### Sync hooks
507
375
 
508
376
  `sync` runs the [pre-sync](#pre-sync-hook) and [post-sync](#post-sync-hook) hooks.
509
377
 
378
+ #### How sync handles writable files
379
+
380
+ When you switch branches with `git checkout`, git removes the read-only flag on every file it writes. This means
381
+ that when you switch back to `main` for a sync, Perforce may refuse to overwrite those files because they are
382
+ writable. git-p4son handles these automatically by classifying each writable file:
383
+
384
+ **writable-unchanged** - The file on disk is identical to the last sync (only the read-only flag was removed
385
+ by git). This is the common case after switching branches. git-p4son detects this by comparing git blob IDs against
386
+ the last sync commit that touched the file, restores the read-only flag, and lets the sync overwrite the file.
387
+
388
+ **writable-changed** - The file on disk differs from the last sync. This happens when you manually edit a file
389
+ on `main` and commit it to git without submitting to Perforce. git-p4son lets the sync overwrite the file and
390
+ includes it in the sync commit (so the commit reflects pure Perforce state), then three-way merges your local
391
+ changes on top of the new Perforce content - similar to a `git rebase`. If the merge is clean, the result appears
392
+ as an unstaged change. If there are conflicts, standard conflict markers are inserted for you to resolve.
393
+
394
+ **added both locally and in Perforce** - A file you committed to git is also being added by Perforce (p4 has never
395
+ synced it to your workspace). There is no common baseline, so git-p4son merges the two versions against an empty
396
+ base: identical content merges cleanly, otherwise the file gets a single conflict block containing both full
397
+ versions for you to resolve.
398
+
399
+ **binary files** - Binary files that have local changes cannot be three-way merged. git-p4son restores your local
400
+ version to disk after the sync commit so it shows up in `git status` for you to handle manually.
401
+
402
+ **added locally, deleted upstream** - A file with local changes in git was deleted in Perforce. git-p4son lets the
403
+ delete stand and warns you - your version remains available in git history.
404
+
405
+ **deleted locally, added upstream** - A file you deleted from git was modified in Perforce. The Perforce version is
406
+ kept in the sync commit. You can delete it again if you want.
407
+
408
+ **ignored** - Files ignored by `.gitignore` are skipped with a warning. These are outside git's domain and should
409
+ be managed manually or via Perforce directly.
410
+
411
+ After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
412
+ commit any files that need attention.
413
+
414
+ **Workspaces with `allwrite`** - When your client spec has the `allwrite` option, every file is writable by design,
415
+ so the read-only flag carries no information about local changes. git-p4son classifies files exactly as above (the
416
+ verdict comes from git, not from the flag), but leaves the write bit alone on files it finds unchanged: Perforce
417
+ compares digests before refusing to overwrite a writable file, and unchanged files pass that check. Only files with
418
+ local changes are made read-only for the duration of the sync, since those are the ones Perforce would otherwise
419
+ refuse to overwrite. The same digest check applies to git-ignored files: unchanged ones sync normally, and after the
420
+ sync git-p4son lists the ones Perforce skipped because they were modified locally.
421
+
422
+ **Writable mode** - With [writable mode](#writable-command) on, the files a sync touches are made writable again after
423
+ the sync if git tracks them. The sync itself works as described above, since the workspace is not `allwrite`.
424
+
510
425
  ### Sync-Split-Users Command
511
426
 
512
427
  Show or edit the [split users](#split-users), the Perforce users whose changelists `sync` gives a commit each:
@@ -531,16 +446,13 @@ bash, zsh and PowerShell run `$(user)` as a command substitution.
531
446
 
532
447
  ### New Command
533
448
 
534
- Create a new Perforce changelist and add changed files to it. Description will contain an enumerated list of git commits
535
- since the base branch. Optionally creates a Swarm review.
536
-
537
- Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
538
- is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
539
- [Keeping the changelist in sync with git](#keeping-the-changelist-in-sync-with-git)).
449
+ Create a changelist from the commits since the base branch: their files are opened in it, and its description lists
450
+ the commits. Optionally shelve it, or create a review in Swarm or another [review tool](#review-tools):
540
451
 
541
452
  ```sh
542
453
  git p4son new -b BASE_BRANCH [-m <message> | -F <file>] [alias] [--force] [--dry-run] [--no-edit]
543
- [--no-alias] [--no-commit-list] [--shelve] [--review] [--per-commit]
454
+ [--no-alias] [--no-commit-list] [--shelve] [--review] [--review-tool NAME] [--per-commit]
455
+ [--ignore-out-of-date]
544
456
  ```
545
457
 
546
458
  **Arguments:**
@@ -553,24 +465,24 @@ git p4son new -b BASE_BRANCH [-m <message> | -F <file>] [alias] [--force] [--dry
553
465
  - `--no-commit-list`: Leave the enumerated commit list out of the changelist description, so it holds only the
554
466
  message
555
467
  - `-b, --base-branch BASE_BRANCH`: Required. The base the change starts from, for enumerating commits and finding
556
- changed files: a branch or bookmark, or a commit exactly one points at. The alias records it by name, as a commit
557
- would go stale after a sync and rebase, along with the commits since it the changelist includes, so `update`
558
- knows where the next ones start. With `--no-alias` any commit will do
468
+ changed files: a branch or bookmark, or a commit exactly one points at, or with jj a change without a bookmark. The
469
+ alias records it by name, or a jj change by its change ID, as a commit would go stale after a sync and rebase, along
470
+ with the commits since it the changelist includes, so `update` knows where the next ones start. With `--no-alias`
471
+ any commit will do
559
472
  - `-f, --force`: Overwrite an existing alias file
560
473
  - `-n, --dry-run`: Pretend and print what would be done, but do not execute. Ends with `Dry run, nothing changed`
561
474
  - `--no-edit`: Skip opening changed files for edit in Perforce
562
475
  - `--no-alias`: Skip saving a changelist alias
563
476
  - `--shelve`: Shelve the changelist after creating it
564
- - `--review`: Add `#review` keyword and shelve to create a Swarm review
477
+ - `--review`: Shelve and create a review with the default [review tool](#review-tools), Swarm unless
478
+ `[review-tool] default` names another. For Swarm, adds the `#review` keyword
479
+ - `--review-tool NAME`: Push the review to this review tool instead of the default one: `swarm`, or a
480
+ `[review-tool.NAME]` in `.git-p4son/config.toml`. Implies `--review`. The alias records the tool, which
481
+ `update --shelve` then runs
565
482
  - `--per-commit`: Create the changelist on the first commit since the base branch, then update it once per later
566
483
  commit, as described below. Refuses `--no-edit` and `--no-alias`
567
-
568
- With `--per-commit`, `new` checks out the first commit since the base branch and runs `git p4son new` on it, then
569
- checks out each later commit and runs `git p4son update` on it, and finally returns to where you started. With
570
- `--shelve` or `--review`, every step shelves, so each commit shows up as its own patch in the Swarm review, and every
571
- step but the last sleeps 5 seconds to give Perforce/Swarm time to process the shelf. It needs a clean git
572
- workspace even with `--dry-run`. [`review`](#review-command) is `new --review --per-commit`, and describes the steps,
573
- the dry run and resuming after a failed step.
484
+ - `--ignore-out-of-date`: Shelve even if files in the change are not at the latest revision (see
485
+ [Shelving only files at the latest revision](#shelving-only-files-at-the-latest-revision))
574
486
 
575
487
  **Examples:**
576
488
  ```sh
@@ -581,67 +493,28 @@ git p4son new -m "New feature" --review -b main
581
493
  git p4son new -m "New feature" --shelve --per-commit -b main # a shelf per commit, without a Swarm review
582
494
  ```
583
495
 
496
+ Opening files requires a clean git workspace, untracked files included; `new` refuses to run otherwise. The check
497
+ is skipped with `--no-edit`. After opening files, unchanged ones are reverted (see
498
+ [Keeping the changelist in sync with git](#keeping-the-changelist-in-sync-with-git)).
499
+
500
+ With `--per-commit`, `new` checks out the first commit since the base branch and runs `git p4son new` on it, then
501
+ checks out each later commit and runs `git p4son update` on it, and finally returns to where you started. With
502
+ `--shelve` or `--review`, every step shelves, so each commit shows up as its own patch in the review. With Swarm,
503
+ every step but the last sleeps 5 seconds to give Perforce/Swarm time to process the shelf; another review tool runs
504
+ once per step instead. It needs a clean git workspace even with `--dry-run`. [`review`](#review-command) is
505
+ `new --review --per-commit`, and describes the steps, the dry run and resuming after a failed step.
506
+
584
507
  ### Update Command
585
508
 
586
- Update an existing Perforce changelist with the commits it doesn't include yet. The message above the commit list
587
- is kept unless `-m` or `-F` replaces it. By default also opens changed files for edit, and reverts files that are no
588
- longer part of the git change (see
589
- [Keeping the changelist in sync with git](#keeping-the-changelist-in-sync-with-git)). Opening files requires a
590
- clean git workspace, untracked files included; `update` refuses to run otherwise. The check is skipped with
591
- `--no-edit`.
592
-
593
- **Changelists made by `new` or `review`.** Their alias records the base, by name, and the commits since it the
594
- changelist includes, by their jj change IDs or, with git, their author dates, which survive rebases and amends.
595
- `update` folds in the commits it doesn't know: it appends their subjects to the commit list, opens their files, and
596
- records them. The entries the list already has are left as they are, so an entry you reworded in the changelist
597
- stays as you wrote it. Every update counts, shelved or not.
598
-
599
- - `-b` only chooses which files to open, from that commit on, for example `-b main` to open them all again after a
600
- manual `p4 revert`. The commit list is still only appended to. A `-b` above the last included commit is refused,
601
- as the commits it skips would be counted without being opened.
602
- - An included commit that is gone from the branch, dropped or squashed into another, gets a warning, and the update
603
- goes on with the new commits. Its patch stays in the review: to undo a shelved commit, revert it in a new commit.
604
- - A new commit below included ones is refused, as its own patch would undo the included commits above it. Move it
605
- after them, then update again.
606
- - An amend, or a squash into an included commit, keeps the commit's identity, so `update` sees no new commit. With
607
- nothing new, `update` opens every file since the base, which takes in what the amend added. With new commits as
608
- well, run `update -b main` to open them.
609
-
610
- **After a sync.** A shelf holds whole files, so once a sync has changed files the changelist touches, its next shelf
611
- would mix those changes into the next commit's patch. So when `update --shelve` finds that the base moved since the
612
- last shelve, the branch was rebased onto it, and it changed such a file, it first inserts an empty commit, `Rebase
613
- onto CL <N>`, right after the last included commit, with the commits after it rebased onto it. Its shelf is a patch
614
- of its own, holding what the sync changed, and its entry keeps the commit list matching the patches.
615
-
616
- - With `--per-commit`, it's the first step. Without it, `update --shelve` shelves twice: at the rebase commit, then at
617
- the tip.
618
- - An update without `--shelve` leaves the catch-up to the next one that shelves, and a changelist that was never
619
- shelved has no patches to catch up. A dry run says what it would insert.
620
-
621
- **Other changelists**, whose alias records nothing (made by `alias new` or an older git-p4son), or named by a number
622
- no alias holds: the commits since the base branch, `HEAD~1` by default, replace their existing entries in the
623
- commit list (matched by subject) and new ones are appended; entries outside the range are kept and the list is
624
- renumbered.
625
-
626
- With `--per-commit`, `update` runs once per commit instead of once for the whole range, the way `new --per-commit`
627
- and `review` do after creating the changelist. It checks out each commit in turn, oldest first, runs the update on
628
- it, and returns to where you started. With `--shelve`, every commit gets a shelf of its own and so shows up as its
629
- own patch in the Swarm review.
630
-
631
- - **With a record,** plain `update --per-commit --shelve` runs on exactly the commits the changelist doesn't include
632
- yet. `-b` is refused there, as it would shelve included commits again.
633
- - **Without one,** pass the range: `update -b HEAD~2 --per-commit --shelve` updates the changelist with the last two
634
- commits, one at a time. The base is resolved once, before the first step, so a relative base like `HEAD~2` means
635
- the same commit for every step.
636
-
637
- `-m`/`-F` is applied on the first step only. It needs a clean git workspace, also with `--no-edit`. If a step
638
- fails, `update` returns to where you started and names the step and commit that failed. The error ends with the
639
- command that resumes after the last commit that worked, so earlier commits get no second shelf; with a record,
640
- running the same command again does the same.
509
+ Add the commits a changelist doesn't include yet: `update` opens their files for edit and appends their subjects to
510
+ the commit list in the description, keeping the message above it unless `-m` or `-F` replaces it. It also reverts
511
+ files that are no longer part of the change (see
512
+ [Keeping the changelist in sync with git](#keeping-the-changelist-in-sync-with-git)). Opening files needs a clean git
513
+ workspace, untracked files included, unless `--no-edit` skips it.
641
514
 
642
515
  ```sh
643
516
  git p4son update [changelist] [-m <message> | -F <file>] [--base-branch BASE_BRANCH] [--dry-run]
644
- [--no-commit-list] [--no-edit] [--shelve] [--per-commit]
517
+ [--no-commit-list] [--no-edit] [--shelve] [--per-commit] [--ignore-out-of-date]
645
518
  ```
646
519
 
647
520
  **Arguments:**
@@ -652,16 +525,23 @@ git p4son update [changelist] [-m <message> | -F <file>] [--base-branch BASE_BRA
652
525
  kept and updated as usual, and so is a Swarm `#review` keyword on a line of its own at the end of the description
653
526
  - `-F, --file FILE`: Like `-m`, but read the message from a file. The message may span multiple lines
654
527
  - `-b, --base-branch BASE_BRANCH`: Where the commits to fold in start, for the commit list and the files to open.
655
- Defaults to after the last commit the changelist includes, when its alias records that, else to `HEAD~1`
528
+ Defaults to after the last commit the changelist includes, when its alias records that, else to `HEAD~1`. When the
529
+ base the alias records no longer resolves, as a deleted branch or an abandoned or divergent jj change, `update`
530
+ refuses without `-b`, and with it records the new base, as `new` does. That base has to sit below the changelist's
531
+ first commit
656
532
  - `-n, --dry-run`: Pretend and print what would be done, but do not execute. Ends with `Dry run, nothing changed`
657
533
  - `--no-commit-list`: Leave the enumerated commit list as it is. Without `-m` or `-F` the description is not
658
534
  touched at all
659
535
  - `--no-edit`: Skip opening changed files for edit in Perforce. With a record, the new commits are then neither
660
536
  appended nor counted, so the next update that opens files includes them
661
- - `--shelve`: Re-shelve the changelist after updating
537
+ - `--shelve`: Re-shelve the changelist after updating. For a review in a [review tool](#review-tools) other than
538
+ Swarm, as the alias records it, the tool then runs for the new shelf
662
539
  - `--per-commit`: Update the changelist once per commit it doesn't include yet, or since the base branch without
663
540
  a record, oldest first, with each commit checked out in turn. With `--shelve`, steps sleep 5 seconds between them
664
- to give Perforce/Swarm time to process each shelf. `--dry-run` prints the steps, one per commit
541
+ to give Perforce/Swarm time to process each shelf, unless another review tool has the changelist. `--dry-run`
542
+ prints the steps, one per commit
543
+ - `--ignore-out-of-date`: Shelve even if files in the change or the changelist are not at the latest revision (see
544
+ [Shelving only files at the latest revision](#shelving-only-files-at-the-latest-revision))
665
545
 
666
546
  **Examples:**
667
547
  ```sh
@@ -674,39 +554,95 @@ git p4son update --per-commit --shelve # one shelf for each commit the changel
674
554
  git p4son update 12345 -b HEAD~2 --per-commit --shelve # no record: the last two commits
675
555
  ```
676
556
 
557
+ #### Which commits are new
558
+
559
+ A changelist made by `new` or `review` has an alias that records its base and the commits it includes, by jj change
560
+ ID or, with git, by author date, which survive rebases and amends. `update` adds only the commits it doesn't know,
561
+ whether it shelves or not, and leaves the entries already in the commit list as they are, so an entry you reworded
562
+ stays as you wrote it.
563
+
564
+ - `-b` only chooses which files to open, for example `-b main` to open them all again after a manual `p4 revert`. A
565
+ `-b` above the last included commit is refused.
566
+ - An included commit that is gone from the branch, dropped or squashed, gets a warning. Its patch stays in the
567
+ review: to undo a shelved commit, revert it in a new commit.
568
+ - A new commit below included ones is refused, as its patch would undo the ones above it. Move it after them.
569
+ - An amend keeps the commit's identity, so `update` sees nothing new and opens every file since the base, which
570
+ takes in the amend. With new commits as well, run `update -b main`.
571
+
572
+ #### After a sync
573
+
574
+ A shelf holds whole files, so after a sync that changed files the changelist touches, the next shelf would mix the
575
+ sync's changes into your next patch. So when `update --shelve` finds you synced and rebased, it first inserts an
576
+ empty commit, `Rebase onto CL <N>`, after the last included commit, and shelves it on its own: its patch holds what
577
+ the sync changed. An update without `--shelve` leaves this to the next one that shelves, and a dry run says what it
578
+ would insert.
579
+
580
+ #### Changelists without a record
581
+
582
+ For a changelist whose alias records no commits, made by `alias new` or an older git-p4son, or one named by a number
583
+ no alias holds, `update` takes the commits since `-b`, `HEAD~1` by default. Their entries replace the ones in the
584
+ commit list with the same subject, the others are appended, and the list is renumbered.
585
+
586
+ #### One update per commit
587
+
588
+ With `--per-commit`, `update` checks out each new commit in turn, oldest first, updates the changelist on it, and
589
+ returns to where you started. With `--shelve`, each commit gets a shelf of its own, so it shows up as its own patch in
590
+ the review. Without a record, give the range with `-b`, such as `-b HEAD~2` for the last two commits; with one, `-b`
591
+ is refused, as it would shelve included commits again. `-m`/`-F` applies to the first step only, and the run needs a
592
+ clean git workspace even with `--no-edit`.
593
+
594
+ If a step fails, `update` returns to where you started and names the commit that failed, with the command that
595
+ resumes after the last commit that worked. With a record, running the same command again does the same.
596
+
677
597
  ### Keeping the changelist in sync with git
678
598
 
679
- `new` and `update` open every file changed since the base branch in the changelist. A file can later stop being
599
+ `new` and `update` open every file changed since the base branch in the changelist. A file git sees as renamed is
600
+ opened as a `p4 move` from its old path to its new one, carrying any edit along, so Perforce keeps its history and the
601
+ review shows the move with only the lines that changed. git finds a rename as `git diff -M` does, by at least half
602
+ the content matching, whatever `diff.renames` says. A file renamed again later moves on from where it started, and an
603
+ existing move stays opened through later edits. A file added earlier in the same change is renamed as an add, as it
604
+ isn't in the depot yet, and a rename that only changes the case of the name is an edit, as a case-insensitive server
605
+ sees one file.
606
+
607
+ A file can later stop being
680
608
  part of the change, for example when a later commit undoes an edit, or a file is added in one commit and deleted
681
609
  in the next. Such files would otherwise stay opened as no-ops, so after opening files both commands revert them:
682
610
 
683
611
  - Files opened for edit that are unchanged compared to the depot are reverted with `p4 revert -a`. Changed files
684
612
  stay opened.
685
613
  - Files opened for add that are missing from disk are reverted.
686
- - Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted and
687
- restored from git.
614
+ - Files opened for delete that git still has (deleted in one commit, restored in a later one) are reverted, keeping
615
+ git's file.
616
+ - Moves whose new path git no longer has, as the rename was undone, are reverted, which reverts both ends.
688
617
 
689
618
  Only files git-p4son has authority over are touched: files tracked by git, and files opened for add that are
690
619
  missing from disk. Anything else in the changelist, such as binaries that only exist in Perforce and were opened
691
- by hand, is left alone even when unchanged. Files opened with other actions (`move/add`, `integrate`, ...) are
692
- also left alone. This step is skipped with `--no-edit`.
620
+ by hand, is left alone even when unchanged. Files opened with other actions (`integrate`, `branch`, ...) are also
621
+ left alone. This step is skipped with `--no-edit`.
693
622
 
694
623
  Shelving (`--shelve`, `--review`) replaces the whole shelf with the changelist's opened files using
695
624
  `p4 shelve -r -a leaveunchanged`, so the shelf never holds stale files, and unchanged files that are still opened
696
625
  are left out of it.
697
626
 
627
+ #### Shelving only files at the latest revision
628
+
629
+ Before shelving, `new`, `update` and `review` check that the files are synced to the latest revision in the depot:
630
+ the change's files, and for `update` also the files already opened in the changelist, or with `--no-edit` only
631
+ those. A file is out of date when someone has submitted a newer revision, deleted it, or added a file of that name
632
+ since your last sync. Shelved, it would show in the review against an older revision, and the changelist would need a
633
+ resolve before it is submitted, so the command lists the files and refuses, before it changes anything. Run
634
+ `git p4son sync`, rebase the change onto it and run the command again, or pass `--ignore-out-of-date` to shelve
635
+ anyway; the command it gives to resume or rerun after a failure keeps the option. With `--per-commit`, the check
636
+ covers every commit once, before the first step. Without shelving there is no check.
637
+
698
638
  ### Review Command
699
639
 
700
- Create a Swarm review with one shelf per commit. `review` is short for `git p4son new --review --per-commit`, taking the
701
- same arguments apart from `--review`, `--per-commit`, `--shelve`, `--no-edit` and `--no-alias`. It checks out the first
702
- commit since the base branch and runs `git p4son new --review` on it, then checks out each later commit and runs
703
- `git p4son update --shelve`, and finally returns to where you started. Every step but the last sleeps 5 seconds to give
704
- Perforce/Swarm time to process the shelf. The commits are reviewed where they are: to review them on top of the latest
705
- `main`, rebase first. Like `new`, it needs a clean git workspace, untracked files included, and checks it even with
706
- `--dry-run`.
640
+ Create a review with one shelf per commit, so each commit is a patch of its own. `review` is short for
641
+ `git p4son new --review --per-commit`:
707
642
 
708
643
  ```sh
709
644
  git p4son review -b BASE_BRANCH [alias] [-m <message> | -F <file>] [--force] [--dry-run] [--no-commit-list]
645
+ [--review-tool NAME] [--ignore-out-of-date]
710
646
  ```
711
647
 
712
648
  **Arguments:**
@@ -722,20 +658,8 @@ git p4son review -b BASE_BRANCH [alias] [-m <message> | -F <file>] [--force] [--
722
658
  - `-n, --dry-run`: Print the steps, one per commit, without running them. Ends with `Dry run, nothing changed`
723
659
  - `--no-commit-list`: Leave the enumerated commit list out of the changelist description. Passed on to `new` and
724
660
  each `update` step
725
-
726
- A dry run prints the plan like this, with the base resolved to the commit every step uses; the message, which only
727
- the first step gets, is printed below it:
728
-
729
- ```
730
- abc1234 First commit: git p4son new my-feature --review -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
731
- def5678 Second commit: git p4son update my-feature --shelve -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
732
- ghi9012 Third commit: git p4son update my-feature --shelve -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
733
- ```
734
-
735
- If a step fails, `review` returns to where you started and names the step and commit that failed. Once the first
736
- step has created the changelist, the error also says how to finish: after fixing the problem, run the
737
- `git p4son update my-feature --shelve --per-commit -b <commit>` it prints, which resumes after the last commit that
738
- worked.
661
+ - `--ignore-out-of-date`: Shelve even if files in the change are not at the latest revision (see
662
+ [Shelving only files at the latest revision](#shelving-only-files-at-the-latest-revision))
739
663
 
740
664
  **Examples:**
741
665
  ```sh
@@ -752,9 +676,30 @@ git p4son review -m "Add my feature" -b main --dry-run
752
676
  git p4son review -F description.txt -b main
753
677
  ```
754
678
 
679
+ `review` checks out the first commit since the base branch and runs `git p4son new --review` on it, then checks out
680
+ each later commit and runs `git p4son update --shelve`, and finally returns to where you started. With Swarm, every
681
+ step but the last sleeps 5 seconds to give Perforce/Swarm time to process the shelf; another
682
+ [review tool](#review-tools) runs once per step instead. The commits are reviewed where they are: to review them on top
683
+ of the latest `main`, rebase first. Like `new`, it needs a clean git workspace, untracked files included, and checks
684
+ it even with `--dry-run`.
685
+
686
+ A dry run prints the plan like this, with the base resolved to the commit every step uses; the message, which only
687
+ the first step gets, is printed below it:
688
+
689
+ ```
690
+ abc1234 First commit: git p4son new my-feature --review -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
691
+ def5678 Second commit: git p4son update my-feature --shelve -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
692
+ ghi9012 Third commit: git p4son update my-feature --shelve -b 5f1d0e9c2a7b4e8d9c0b1a2f3e4d5c6b7a8f9e0d
693
+ ```
694
+
695
+ If a step fails, `review` returns to where you started and names the step and commit that failed. Once the first
696
+ step has created the changelist, the error also says how to finish: after fixing the problem, run the
697
+ `git p4son update my-feature --shelve --per-commit -b <commit>` it prints, which resumes after the last commit that
698
+ worked.
699
+
755
700
  ### List-Changes Command
756
701
 
757
- List commit subjects since a base branch in chronological order (oldest first):
702
+ List the commit subjects since a base branch, oldest first and numbered, as a changelist description lists them:
758
703
 
759
704
  ```sh
760
705
  git p4son list-changes [--base-branch BASE_BRANCH] [--porcelain]
@@ -772,17 +717,12 @@ git p4son list-changes --base-branch main
772
717
  git p4son list-changes --base-branch main --porcelain
773
718
  ```
774
719
 
775
- This command is useful for generating changelist descriptions by listing all commit messages since the base branch,
776
- numbered sequentially.
777
-
778
720
  ### Alias Command
779
721
 
780
- Manage changelist aliases, stored one per file as `.git-p4son/aliases/<alias>.toml` (`changelist = 12345`, and
781
- with the jj backend `change = "<change ID>"`, the [jj change](#jujutsu-jj) it was made for). An alias that `new` or
782
- `review` saved also records the changelist's base and the commits since it it includes, which
783
- [`update`](#update-command) goes on from: `base`, `base_commit` and `included`, a list of the commits' identities.
784
- Aliases saved by older versions in `.git-p4son/changelists/` keep working, and move to `.git-p4son/aliases/` when
785
- written, or all at once with `alias clean`.
722
+ Manage changelist aliases: names that stand for changelist numbers, so you can say `my-feature` instead of `12345`.
723
+ `new` and `review` save one named after the branch, which also records what [`update`](#update-command) needs to go
724
+ on from. Aliases are stored in `.git-p4son/aliases/`; ones saved by older versions in `.git-p4son/changelists/` keep
725
+ working, and move when written, or all at once with `alias clean`.
786
726
 
787
727
  #### alias list
788
728
 
@@ -792,11 +732,6 @@ List all aliases and their changelist numbers:
792
732
  git p4son alias list
793
733
  ```
794
734
 
795
- **Examples:**
796
- ```sh
797
- git p4son alias list
798
- ```
799
-
800
735
  #### alias show
801
736
 
802
737
  Print the changelist number saved under an alias:
@@ -840,6 +775,36 @@ git p4son alias new 12345 myfeature
840
775
  git p4son alias new 67890 myfeature -f
841
776
  ```
842
777
 
778
+ #### alias rename
779
+
780
+ Rename an alias, keeping its changelist and everything it records for `update`:
781
+
782
+ ```sh
783
+ git p4son alias rename [OLD] NEW [--force]
784
+ ```
785
+
786
+ As with `git branch -m`, a single name is the new name for the current alias, the one the current branch names, or
787
+ with jj the one that records the jj change. The `branch` keyword works for either name; as `NEW` it is the current
788
+ branch or bookmark name, so the alias can follow a renamed branch.
789
+
790
+ **Arguments:**
791
+ - `OLD` (optional): The alias to rename. Defaults to the current alias
792
+ - `NEW`: The new alias name
793
+
794
+ **Options:**
795
+ - `-f, --force`: Overwrite an existing alias of the new name
796
+
797
+ **Examples:**
798
+ ```sh
799
+ git p4son alias rename my-feature-v2 # the current alias, then: git branch -m my-feature-v2
800
+ git p4son alias rename my-feature branch # after git branch -m: follow the branch's new name
801
+ git p4son alias rename my-feature my-feature-v2
802
+ ```
803
+
804
+ With git, renaming the branch first means the branch no longer names the alias, so name both:
805
+ `alias rename OLD branch`. With jj, `alias rename NEW` works before or after `jj bookmark rename`, as the alias
806
+ records the jj change.
807
+
843
808
  #### alias delete
844
809
 
845
810
  Delete a changelist alias, or all of them:
@@ -852,8 +817,9 @@ git p4son alias delete [alias | --all]
852
817
  - `alias` (optional): Alias name to delete. Defaults to the current branch name.
853
818
 
854
819
  **Options:**
855
- - `--all`: Delete all aliases without prompting. Unlike `alias clean`, it does not ask first, which suits scripts
856
- and AI agents
820
+ - `--all`: Delete all aliases without prompting, whether their changelists are pending or not. To delete only those
821
+ that are not, use `alias clean`. An alias that can't be deleted is reported and skipped, and the command then exits
822
+ with 1
857
823
 
858
824
  **Examples:**
859
825
  ```sh
@@ -864,27 +830,37 @@ git p4son alias delete --all
864
830
 
865
831
  #### alias clean
866
832
 
867
- Clean up changelist aliases:
833
+ Delete the aliases whose changelist is no longer pending, as it was submitted or deleted:
868
834
 
869
835
  ```sh
870
- git p4son alias clean
836
+ git p4son alias clean [-n] [--porcelain]
871
837
  ```
872
838
 
873
- It first moves any aliases still in `.git-p4son/changelists/` to `.git-p4son/aliases/`, then lists all aliases and
874
- asks how to proceed:
875
- - `a` (all): Delete all aliases
876
- - `i` (interactive): Review each alias one by one
877
- - `q` (quit): Exit without deleting anything
839
+ It first moves any aliases still in `.git-p4son/changelists/` to `.git-p4son/aliases/`, then goes through the aliases
840
+ in name order: it looks up the alias's changelist in Perforce and deletes the alias if the changelist was submitted or
841
+ doesn't exist. If p4 answers neither that nor pending for a changelist, it stops
842
+ there, leaving that alias and the ones after it. A submit that renumbers the changelist leaves its old number behind,
843
+ which counts as gone too. With the jj backend, an alias whose [jj change](#jujutsu-jj) is gone but whose changelist
844
+ is still pending is kept; `alias list` marks it `(gone)`, and `alias delete` removes it.
845
+
846
+ It ends under an `Alias clean done` heading with how many aliases it deleted and kept. If any couldn't be deleted,
847
+ that line is a warning with how many failed, followed by an error, and the command exits with 1.
878
848
 
879
- In interactive mode, each alias is displayed in turn with a prompt:
880
- - `y` (yes): Delete this alias
881
- - `n` (no): Keep this alias
882
- - `a` (all): Delete this and all remaining aliases (handy for keeping the first few and sweeping the rest)
883
- - `q` (quit): Stop and keep remaining aliases
849
+ To delete specific aliases, use `alias list` and `alias delete`.
850
+
851
+ **Options:**
852
+ - `-n, --dry-run`: List the aliases that would be deleted, without deleting or moving any. Ends with
853
+ `Dry run, nothing changed`
854
+ - `--porcelain`: Print only one `<action> <alias> <CL>` line per alias, for scripts and agents. The action is `kept`,
855
+ `deleted`, or `failed` when the alias couldn't be deleted; with `-n` it is `would-keep` or `would-delete`. Errors
856
+ still go to stderr. A `failed` alias makes the exit code 1, after the lines for all aliases; a stop exits with 1,
857
+ with no line for the alias it stopped at or those after it.
884
858
 
885
859
  **Examples:**
886
860
  ```sh
861
+ git p4son alias clean -n # see which aliases would go
887
862
  git p4son alias clean
863
+ git p4son alias clean --porcelain # e.g. "deleted feature-login 12345"
888
864
  ```
889
865
 
890
866
  ### Writable Command
@@ -957,28 +933,116 @@ Most commands that accept an alias or changelist argument default to the current
957
933
  The keyword cannot be used in a detached HEAD state. Use `--no-alias` (on `new`) or supply an explicit
958
934
  alias name instead.
959
935
 
936
+ ## Configuration
937
+
938
+ git-p4son keeps its per-repository settings in `.git-p4son/config.toml`. `init`, `writable` and `sync-split-users`
939
+ write most of them; the rest you edit by hand.
940
+
941
+ - `[depot] root` (string, set by `init`): the depot path `sync` syncs, e.g. `//$(workspace)` or
942
+ `//$(workspace)/Engine`. See [Init Command](#init-command)
943
+ - `[core] writable` (`true` or `false`, default `false`): [writable mode](#writable-command), which keeps
944
+ git-tracked files writable
945
+ - `[core] backend` (`"auto"`, `"git"` or `"jj"`, default `"auto"`): the tool git-p4son follows for the change, the
946
+ base and the branch. `auto` means jj in a [jj workspace](#jujutsu-jj) and git otherwise
947
+ - `[sync] branch` (string, set by `init`): the sync branch, which `sync` commits onto from whatever branch you are
948
+ on. Without it, `sync` uses `main`. See [Sync Command](#sync-command)
949
+ - `[sync] split-users` (list of strings, default empty): the users whose changelists `sync` gives a commit each;
950
+ `$(user)` stands for you. See [Split users](#split-users)
951
+ - `[sync] blocking-processes` (list of strings, default empty): processes that stop a sync while they run. See
952
+ [Blocking processes](#blocking-processes)
953
+ - `[review-tool] default` (string, default `"swarm"`): the [review tool](#review-tools) `new --review` and `review`
954
+ push to without `--review-tool`
955
+ - `[review-tool.<name>]` (table): a [review tool](#review-tools)'s `command`, and its `new` and `update` arguments
956
+ - `[core.script-runners]` (table, Windows only): the command that runs a hook script, by file extension.
957
+ See [Hooks](#hooks). The old `[hooks.extension-associations]` is still read, and `core.script-runners` wins where
958
+ both name an extension
959
+
960
+ For example:
961
+
962
+ ```toml
963
+ [depot]
964
+ root = "//$(workspace)"
965
+
966
+ [core]
967
+ writable = true
968
+
969
+ [sync]
970
+ split-users = ["$(user)", "alice"]
971
+ blocking-processes = ["UnrealEditor"]
972
+ ```
973
+
974
+ A value of the wrong type, such as `writable = "true"` or `split-users = "alice"`, or a file that is not valid TOML,
975
+ stops the command with an error naming the setting, so a typo is never silently ignored.
976
+
977
+ ## Review Tools
978
+
979
+ `new --review` and `review` create a review in Swarm by default: the description gets a `#review` keyword, and Swarm
980
+ picks up each shelf. A team that reviews with another tool, one that has to be run to post each shelf, configures the
981
+ tool's command line under a name of its choosing in `.git-p4son/config.toml`:
982
+
983
+ ```toml
984
+ [review-tool]
985
+ default = "my-tool"
986
+
987
+ [review-tool.my-tool]
988
+ command = ["python", "tools/my-review.py", "--p4port", "$(p4port)", "--changelist", "$(changelist)"]
989
+ new = ["create"]
990
+ update = ["update"]
991
+ ```
992
+
993
+ `--review-tool my-tool` pushes a review to it, and `default` makes it the default, so `--review-tool swarm` picks
994
+ Swarm again. The alias records the changelist's tool, so `update --shelve` runs it again without naming it; to move a
995
+ change to another tool, delete the changelist and its alias and create the review again. `--no-alias` is refused with
996
+ any tool but Swarm.
997
+
998
+ With a review tool other than Swarm, there is no `#review` keyword, and after each shelve of the review git-p4son runs
999
+ `command` from the workspace root, once per commit with `review`. On the shelve of `new`, the `new` arguments are
1000
+ added to it, to create the review, and on the shelves of `update` the `update` ones, to add the new shelf as a
1001
+ revision of it. If the first run fails, the `update --shelve` that reruns it still gets the `new` arguments, as the
1002
+ review doesn't exist yet. Both are optional; without one, `command` runs as it is.
1003
+
1004
+ `command`, `new` and `update` are lists, one string per argument; a single string is refused. No shell runs them,
1005
+ so they work the same on every platform and never need quoting. A program named by a relative path, such as
1006
+ `tools/review.exe`, is found from the workspace root, and a bare name, such as `python`, on `PATH`. A `.cmd` or `.ps1`
1007
+ script on Windows needs its interpreter named, such as `["pwsh", "-File", "tools/my-review.ps1"]`. `$(name)` in an
1008
+ argument is replaced:
1009
+
1010
+ - `$(changelist)`: the pending changelist number
1011
+ - `$(workspace)`: the Perforce client workspace name
1012
+ - `$(user)`: the Perforce user
1013
+ - `$(p4port)`: the server address, as `p4 set P4PORT` gives it
1014
+ - any other name: the environment variable of that name, which on Windows ignores case. A name that is neither fails
1015
+ the command before anything runs
1016
+
1017
+ A replaced value stays within its argument, spaces and all. The command is echoed with the values in, so it can be
1018
+ rerun by hand; that also shows any token passed through an environment variable. Anything more, such as settings
1019
+ files or a sign-in, goes in a wrapper script of your own that the command runs.
1020
+
1021
+ The first run can sign in or open a browser; later ones run once per shelve, often as the steps of a per-commit run,
1022
+ so keep them non-interactive. A tool that exits with a non-zero code fails the command. `review` then stops at that
1023
+ commit with the command that resumes it, which shelves the commit again and reruns the tool, and `new` names the
1024
+ `update <alias> --shelve` that reruns it once the tool works. `--dry-run` says which tool would run. The tool runs on
1025
+ the terminal, so its output shows as it is written and it can prompt. Like a hook, it runs under git-p4son's
1026
+ [jj lock](#locking-and-the-git-workflow), so it must not run jj commands that snapshot.
1027
+
960
1028
  ## Hooks
961
1029
 
962
- Hooks work similarly to git hooks. They are executables (unix)/scripts (windows) that you place in
963
- the `.git-p4son/hooks/[hook-name]/` directory. These scripts will be called to add or modify behavior
964
- at defined times during git-p4son operations. Scripts get run in filename (lexicographically) sorted
965
- order.
1030
+ Like git hooks, hooks are programs git-p4son runs at set points of a command: executables, or on Windows scripts, in
1031
+ `.git-p4son/hooks/<hook-name>/`, run in filename order.
966
1032
 
967
- The hook process CWD is the directory where git-p4son was invoked, and `GIT_P4SON_REPO_ROOT_DIR` is
968
- set to the git-p4son repository root. `GIT_P4SON_BACKEND` is `git` or `jj`, the [backend](#choosing-the-backend)
969
- the command runs with.
1033
+ A hook runs in the directory git-p4son was run from, with `GIT_P4SON_REPO_ROOT_DIR` set to the repository root and
1034
+ `GIT_P4SON_BACKEND` to `git` or `jj`, the [backend](#choosing-the-backend) the command runs with.
970
1035
 
971
- On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with
972
- `powershell.exe -NoProfile -ExecutionPolicy Bypass -File`, `nu.exe`, `bash.exe`, and `python.exe` respectively.
973
- Customize or add Windows script associations in `.git-p4son/config.toml`:
1036
+ On Windows, `.ps1`, `.sh` and `.py` files run with `powershell.exe -NoProfile -ExecutionPolicy Bypass -File`,
1037
+ `bash.exe` and `python.exe`. Change or add script runners in `.git-p4son/config.toml`:
974
1038
 
975
1039
  ```toml
976
- [hooks.extension-associations]
1040
+ [core.script-runners]
977
1041
  ".ps1" = ["pwsh.exe", "-NoProfile", "-File"]
978
- ".py" = "python.exe"
1042
+ ".nu" = "nu.exe"
979
1043
  ```
980
1044
 
981
- Non-executable or unrecognized hook files, and invalid association entries, are skipped with a warning.
1045
+ Non-executable or unrecognized hook files, and invalid runner entries, are skipped with a warning.
982
1046
 
983
1047
  ### pre-sync hook
984
1048
 
@@ -1044,13 +1108,20 @@ With the jj backend in a jj workspace:
1044
1108
  - `new` records the change's ID in the alias it saves, so later commands find the alias by change, whatever the
1045
1109
  change's bookmarks or description have become since. A new alias is named after the one local bookmark at the
1046
1110
  change, with `/` replaced by `-`, or, with no bookmark or several, after the first 8 letters of its change ID.
1111
+ - When no alias records the change itself, as after adding changes on top of the one `new` recorded, the current
1112
+ alias is the one recording the nearest change below it. The search stops at the first bookmark below the change,
1113
+ so a bookmarked change stacked on another's CL doesn't take that CL's alias, and skips empty changes, which a sync
1114
+ leaves behind for submitted CLs. In a stack without bookmarks, every change above `main` counts. jj doesn't move
1115
+ a bookmark on `jj new`, so a bookmark left on the change `new` recorded stops the search from the changes above
1116
+ it: move it to the tip with `jj bookmark move <name> --to @-`, or `--to @` when the working change has content.
1047
1117
  - An alias records one change, and a change one alias: `new` refuses a change another alias records, naming it, and
1048
1118
  `--force` moves the record to the new alias. `alias new` records no change. `alias list` shows each recorded change,
1049
1119
  marked `(gone)` once jj no longer has it.
1050
1120
  - The default message is the first line of the change's description. Use `jj describe`, `-m` or `-F`.
1051
1121
  - `-b/--base-branch` takes a jj revision that resolves to one change, such as a bookmark, change ID or commit ID.
1052
1122
  Where it has a default, it's the change's parent rather than git's `HEAD~1`. `new` and `review` record it as a
1053
- bookmark, so it has to name one, or resolve to a commit exactly one points at.
1123
+ bookmark when it names one or resolves to a commit exactly one points at, and as its change ID when it has none, so
1124
+ a local change kept under a stack, never submitted, can be the base. Several bookmarks at it is an error.
1054
1125
  - `new` and `update` use jj's snapshot of the working copy, check that it did not change while git-p4son took the
1055
1126
  lock, and refuse files jj left untracked. You do not need a separate committed change before opening files in
1056
1127
  Perforce. Before a Perforce revert overwrites a file, git-p4son saves the working file and puts it back
@@ -1147,44 +1218,35 @@ colocates with both settings.
1147
1218
  git matches `.gitignore` patterns without case when `core.ignorecase` is true, the default on Windows and macOS. jj
1148
1219
  always matches them with case, so it would track a `Thirdparty/` folder that git ignores through `ThirdParty/`.
1149
1220
 
1150
- To keep jj from tracking them, git-p4son leaves each such folder, or file, out of jj's `snapshot.auto-track` for the
1151
- repository, building on any value you have, for example `(all()) ~ root:Some/Plugin/Source/Thirdparty`. That's
1152
- jj's own setting, so it works whatever `.gitignore` re-includes with `!`, which would outrank `.git/info/exclude`,
1153
- and it leaves git alone. `jj status` then lists the folders under "Untracked paths". As the repository's value holds
1154
- yours, a later change to your user-level `snapshot.auto-track` doesn't reach the repository.
1155
-
1156
- git-p4son does so when `init` colocates, passing the setting to `jj git init`, as jj snapshots straight away, and in
1157
- the steps it lists for colocating a repository it creates by hand. It also does so at the end of each sync on the jj
1158
- backend, as a sync can bring in new folders. To let jj track such a folder again, remove its `root:"..."` from
1159
- `snapshot.auto-track`, with `jj config edit --repo`.
1221
+ git-p4son keeps jj from tracking them by leaving each such folder, or file, out of jj's `snapshot.auto-track` for the
1222
+ repository, building on any value you have, for example `(all()) ~ root:Some/Plugin/Source/Thirdparty`. `jj status`
1223
+ then lists them under "Untracked paths". It does so when `init` colocates, and at the end of each sync on the jj
1224
+ backend, as a sync can bring in new folders. To let jj track such a folder again, remove its `root:"..."` with
1225
+ `jj config edit --repo`. As the repository's value holds yours, a later change to your user-level
1226
+ `snapshot.auto-track` doesn't reach the repository.
1160
1227
 
1161
1228
  If jj tracks such files anyway, for example in a workspace colocated before git-p4son did this, git-p4son warns about
1162
- the ones the change you're working on adds, unless it's a commit in the sync branch's history, which git made. The
1163
- warning lists the `jj config set` that leaves them out and the `jj file untrack` command that stops tracking them,
1164
- which leaves the files on disk. Both are quoted to paste into cmd, PowerShell or a POSIX shell alike. They name a
1165
- folder only when git tracks nothing else in it, so files git tracks stay tracked. Files you track on purpose, with
1166
- `jj file track --include-ignored`, are left alone.
1229
+ the ones your change adds. The warning lists the `jj config set` that leaves them out and the `jj file untrack` that
1230
+ stops tracking them, which leaves the files on disk. Both are quoted to paste into cmd, PowerShell or a POSIX shell.
1231
+ They name a folder only when git tracks nothing else in it, so files git tracks stay tracked. Files you track on
1232
+ purpose, with `jj file track --include-ignored`, are left alone.
1167
1233
 
1168
1234
  ### Locking, and the git workflow
1169
1235
 
1170
1236
  git-p4son holds jj's working-copy lock while commands run, including sync hooks and temporary Perforce reverts,
1171
- so other jj processes cannot snapshot files halfway through. A busy lock stops the command. Queries during the
1172
- command use a pinned jj operation without snapshotting.
1173
-
1174
- jj's first snapshot and final reconciliation happen outside the lock, and so do sync's rebase and return, so
1175
- another jj command can import the finished git changes before git-p4son does: this is not one atomic jj
1176
- transaction. `--verbose` logs the jj operation git-p4son pinned at the start. The lock does not keep out
1177
- editors, git commands or other jj workspaces. If a command fails, look at the workspace before continuing: the
1178
- lock is released, but Perforce operations that completed are not undone.
1237
+ so other jj processes cannot snapshot files halfway through. A busy lock stops the command. The lock does not keep
1238
+ out editors, git commands or other jj workspaces, and a command is not one atomic jj transaction: jj's first
1239
+ snapshot, the final import, and sync's rebase and return happen outside it. If a command fails, look at the workspace
1240
+ before continuing: Perforce operations that completed are not undone.
1179
1241
 
1180
1242
  Hooks must not run ordinary jj commands, which would wait for the lock their caller holds. Read-only queries such
1181
- as `jj --at-op=@ log` avoid snapshotting and the git import. A hook that runs git-p4son itself needs
1182
- `--backend git`, which reuses the caller's lock.
1243
+ as `jj --at-op=@ log` are fine, and a hook that runs git-p4son itself needs `--backend git`, which reuses the
1244
+ caller's lock.
1183
1245
 
1184
- `review`, `new --per-commit` and `update --per-commit` run once per commit since the base, up to the jj change, as they
1185
- do in git. Under jj's lock, each step puts its commit's files on disk with git, and the working change's files go back
1186
- at the end, also when a step fails, so jj sees no change. No jj command runs, and untracked and ignored files are left
1187
- alone. They refuse to start if files changed after jj's snapshot, as the checkouts would overwrite them:
1246
+ `review`, `new --per-commit` and `update --per-commit` run once per commit since the base, as they do in git. Under
1247
+ jj's lock, each step puts its commit's files on disk with git, and the working change's files go back at the end,
1248
+ also when a step fails, so jj sees no change. Untracked and ignored files are left alone. They refuse to start if
1249
+ files changed after jj's snapshot, as the checkouts would overwrite them:
1188
1250
 
1189
1251
  ```sh
1190
1252
  git p4son review -b main
@@ -1277,3 +1339,39 @@ source $(git-p4son completion zsh --jj) # ~/.zshrc, after compinit
1277
1339
  Each loads the `git p4son` completion for its shell too. If you named the alias something other than `p4son`, set
1278
1340
  `GIT_P4SON_JJ_ALIAS` to its name.
1279
1341
 
1342
+ ## Development
1343
+
1344
+ To install from source in editable mode:
1345
+
1346
+ ```sh
1347
+ git clone https://github.com/neoboid/git-p4son.git
1348
+ cd git-p4son
1349
+ pip install -e .
1350
+ ```
1351
+
1352
+ The `-e` flag installs the package in "editable" mode, meaning changes to the code are immediately available
1353
+ without reinstalling. This is also handy if you want to auto-update git-p4son whenever you pull from GitHub.
1354
+
1355
+ git-p4son only uses Python standard library modules - no additional packages are required.
1356
+
1357
+ Run the tests with `python -m pytest tests/` (`pip install pytest`). With `pytest-xdist` installed, add
1358
+ `-n auto --maxprocesses 8` to run them in parallel, which is about three times faster.
1359
+
1360
+ ### Git Hooks
1361
+
1362
+ The repository includes git hooks in the `hooks/` directory:
1363
+
1364
+ - **pre-commit** - verifies that staged Python files are formatted with `autopep8`. Install it with
1365
+ `pip install autopep8`.
1366
+ - **pre-push** - runs the test suite and blocks the push if any test fails. It needs `pytest` installed, and runs
1367
+ the tests in parallel when `pytest-xdist` is too.
1368
+
1369
+ To activate the hooks, run:
1370
+
1371
+ ```sh
1372
+ scripts/setup-hooks.sh
1373
+ ```
1374
+
1375
+ This configures `core.hooksPath` so git uses the hooks from the repository. You only need to run this once after
1376
+ cloning.
1377
+