git-p4son 0.2.11__tar.gz → 0.3.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 (54) hide show
  1. {git_p4son-0.2.11 → git_p4son-0.3.1}/PKG-INFO +68 -27
  2. {git_p4son-0.2.11 → git_p4son-0.3.1}/README.md +67 -26
  3. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/alias.py +3 -27
  5. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/changelist_store.py +1 -1
  6. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/cli.py +13 -9
  7. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/common.py +25 -0
  8. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/complete.py +10 -5
  9. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/config.py +28 -10
  10. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/git.py +193 -0
  11. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/init.py +31 -28
  12. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/perforce.py +75 -8
  13. git_p4son-0.3.1/git_p4son/state.py +56 -0
  14. git_p4son-0.3.1/git_p4son/sync.py +778 -0
  15. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son.egg-info/PKG-INFO +68 -27
  16. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son.egg-info/SOURCES.txt +3 -0
  17. {git_p4son-0.2.11 → git_p4son-0.3.1}/pyproject.toml +1 -1
  18. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_alias.py +4 -3
  19. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_changelist_store.py +5 -0
  20. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_cli.py +7 -3
  21. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_complete.py +15 -0
  22. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_config.py +22 -1
  23. git_p4son-0.3.1/tests/test_git.py +407 -0
  24. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_init.py +37 -16
  25. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_lib_edit.py +25 -4
  26. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_perforce.py +107 -1
  27. git_p4son-0.3.1/tests/test_state.py +64 -0
  28. git_p4son-0.3.1/tests/test_sync.py +1573 -0
  29. git_p4son-0.2.11/git_p4son/sync.py +0 -208
  30. git_p4son-0.2.11/tests/test_sync.py +0 -497
  31. {git_p4son-0.2.11 → git_p4son-0.3.1}/LICENSE +0 -0
  32. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/__main__.py +0 -0
  33. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/completions/_git-p4son +0 -0
  34. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/completions/git-p4son.bash +0 -0
  35. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/completions/git-p4son.ps1 +0 -0
  36. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/hooks.py +0 -0
  37. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/lib.py +0 -0
  38. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/list_changes.py +0 -0
  39. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/log.py +0 -0
  40. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/new.py +0 -0
  41. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/review.py +0 -0
  42. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son/update.py +0 -0
  43. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son.egg-info/dependency_links.txt +0 -0
  44. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son.egg-info/entry_points.txt +0 -0
  45. {git_p4son-0.2.11 → git_p4son-0.3.1}/git_p4son.egg-info/top_level.txt +0 -0
  46. {git_p4son-0.2.11 → git_p4son-0.3.1}/setup.cfg +0 -0
  47. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_common.py +0 -0
  48. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_hooks.py +0 -0
  49. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_lib_changelist.py +0 -0
  50. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_lib_review.py +0 -0
  51. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_list_changes.py +0 -0
  52. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_log.py +0 -0
  53. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_new.py +0 -0
  54. {git_p4son-0.2.11 → git_p4son-0.3.1}/tests/test_review.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.2.11
3
+ Version: 0.3.1
4
4
  Summary: Utility for keeping a Perforce workspace and local git repo in sync
5
5
  Author-email: Andreas Andersson <andreas@neoboid.com>
6
6
  License-Expression: MIT
@@ -156,41 +156,37 @@ cloning.
156
156
 
157
157
  These steps set up git-p4son in an existing Perforce workspace. You only need to do this once.
158
158
 
159
- 1. **Enable clobber on your Perforce workspace.** Edit the workspace in
160
- P4V to set the clobber flag, or run `p4 client` and change `noclobber` to `clobber` in the Options line.
161
- See [Why clobber?](#why-clobber) for rationale.
162
-
163
- 2. **Set a git editor** if you don't have one already. The `review` command opens an interactive rebase in your
159
+ 1. **Set a git editor** if you don't have one already. The `review` command opens an interactive rebase in your
164
160
  editor. If you haven't configured one, set it with:
165
161
  ```sh
166
162
  git config --global core.editor <editor> # e.g. vim, nano, code --wait
167
163
  ```
168
164
 
169
- 3. **Sync your workspace to a known changelist.** Pick a changelist to use as the starting point for your git
165
+ 2. **Sync your workspace to a known changelist.** Pick a changelist to use as the starting point for your git
170
166
  history:
171
167
  ```sh
172
168
  p4 sync //...@12345
173
169
  ```
174
170
 
175
- 4. **Run `git p4son init`.** This can be anywhere inside your Perforce workspace — it doesn't have to be at the
171
+ 3. **Run `git p4son init`.** This can be anywhere inside your Perforce workspace - it doesn't have to be at the
176
172
  root:
177
173
  ```sh
178
174
  cd /path/to/your/workspace # or a subdirectory of it
179
175
  git p4son init
180
176
  ```
181
- The command verifies that you are inside a P4 workspace with clobber enabled, prompts you to select a depot
177
+ The command verifies that you are inside a P4 workspace, prompts you to select a depot
182
178
  root (entire workspace or current directory subtree), runs `git init`, sets up `.gitignore` (copying from
183
179
  `.p4ignore` if available), and creates an initial commit.
184
180
 
185
- 5. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded.
181
+ 4. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded.
186
182
 
187
- 6. **Add and commit all files manually the first time**
183
+ 5. **Add and commit all files manually the first time**
188
184
  ```sh
189
185
  git add .
190
186
  git commit -m "Initial submit all files"
191
187
  ```
192
188
 
193
- 7. **Run `git p4son sync`** to get your first official sync commit
189
+ 6. **Run `git p4son sync`** to get your first official sync commit
194
190
  ```sh
195
191
  git p4son sync
196
192
  ```
@@ -199,19 +195,41 @@ These steps set up git-p4son in an existing Perforce workspace. You only need to
199
195
  From here, branch off `main` for local development. See the [Usage Example](#usage-example) for a typical
200
196
  workflow.
201
197
 
202
- ### Why clobber?
198
+ ### How sync handles writable files
199
+
200
+ When you switch branches with `git checkout`, git removes the read-only flag on every file it writes. This means
201
+ that when you switch back to `main` for a sync, Perforce may report "Can't clobber writable file" errors. git-p4son
202
+ handles these automatically by classifying each writable file:
203
+
204
+ **writable-unchanged** - The file on disk is identical to the last sync (only the read-only flag was removed
205
+ by git). This is the common case after switching branches. git-p4son detects this by comparing git blob IDs against
206
+ the last sync commit that touched the file, restores the read-only flag, and lets the sync overwrite the file.
207
+
208
+ **writable-changed** - The file on disk differs from the last sync. This happens when you manually edit a file
209
+ on `main` and commit it to git without submitting to Perforce. git-p4son lets the sync overwrite the file and
210
+ includes it in the sync commit (so the commit reflects pure Perforce state), then three-way merges your local
211
+ changes on top of the new Perforce content - similar to a `git rebase`. If the merge is clean, the result appears
212
+ as an unstaged change. If there are conflicts, standard conflict markers are inserted for you to resolve.
213
+
214
+ **added both locally and in Perforce** - A file you committed to git is also being added by Perforce (p4 has never
215
+ synced it to your workspace). There is no common baseline, so git-p4son merges the two versions against an empty
216
+ base: identical content merges cleanly, otherwise the file gets a single conflict block containing both full
217
+ versions for you to resolve.
218
+
219
+ **binary files** - Binary files that have local changes cannot be three-way merged. git-p4son restores your local
220
+ version to disk after the sync commit so it shows up in `git status` for you to handle manually.
203
221
 
204
- When you switch branches with `git checkout`, git overwrites the files that differ between the source and target branch.
205
- In doing so, it always removes the read-only flag - this is just how git works.
222
+ **added locally, deleted upstream** - A file with local changes in git was deleted in Perforce. git-p4son lets the
223
+ delete stand and warns you - your version remains available in git history.
206
224
 
207
- Now imagine one of those files has also been modified in Perforce. The next time `p4 sync` runs, Perforce will complain
208
- that a file was changed outside its control. At that point you have two options: either force-sync the individual file
209
- with `p4 sync -f //path/to/file`, or enable the `clobber` flag on your workspace so that Perforce silently overwrites
210
- writable files.
225
+ **deleted locally, added upstream** - A file you deleted from git was modified in Perforce. The Perforce version is
226
+ kept in the sync commit. You can delete it again if you want.
211
227
 
212
- This is not specific to git-p4son - any workflow that combines git and Perforce in the same workspace will run into it.
213
- And in practice it is harmless: your local changes live on feature branches in git, so when you rebase onto `main` after
214
- a sync, your changes are reapplied on top of the latest Perforce state.
228
+ **ignored** - Files ignored by `.gitignore` are skipped with a warning. These are outside git's domain and should
229
+ be managed manually or via Perforce directly.
230
+
231
+ After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
232
+ commit any files that need attention.
215
233
 
216
234
  ## Usage
217
235
 
@@ -241,13 +259,18 @@ Initialize a git repository inside a Perforce workspace:
241
259
  git p4son init
242
260
  ```
243
261
 
244
- This command checks preconditions (Perforce workspace, clobber flag), configures the depot root, runs `git init`,
262
+ This command checks preconditions (Perforce workspace), configures the depot root, runs `git init`,
245
263
  sets up `.gitignore`, and creates an initial commit.
246
264
 
247
265
  The depot root determines which part of the Perforce workspace git-p4son syncs. You can choose to sync the entire
248
266
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
249
267
  `.git-p4son/config.toml` and used by all subsequent commands.
250
268
 
269
+ The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
270
+ `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
271
+ the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
272
+ by hand-editing `config.toml`; both forms work.
273
+
251
274
  The `.gitignore` is set up using this priority:
252
275
  - If `.gitignore` already exists, it is left as is
253
276
  - If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
@@ -258,24 +281,35 @@ The `.gitignore` is set up using this priority:
258
281
  Sync local git repository with a Perforce workspace:
259
282
 
260
283
  ```sh
261
- git p4son sync [changelist] [--force]
284
+ git p4son sync [changelist ...] [--force]
262
285
  ```
263
286
 
264
287
  **Arguments:**
265
- - `changelist` (optional): Changelist number, or `last-synced` to re-sync the last synced changelist. Omit to
266
- sync to the latest changelist affecting the workspace.
288
+ - `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
289
+ to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
290
+ strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
291
+ changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
267
292
 
268
293
  **Options:**
269
- - `-f, --force`: Force sync encountered writable files and allow syncing to older changelists.
294
+ - `-f, --force`: Allow syncing to changelists older than the current one.
270
295
 
271
296
  **Examples:**
272
297
  ```sh
273
298
  git p4son sync # sync to latest
299
+ git p4son sync head # sync to latest (explicit)
274
300
  git p4son sync 12345
301
+ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
302
+ git p4son sync 123 156 head # sync 123, 156, then the latest changelist
275
303
  git p4son sync last-synced
276
304
  git p4son sync 12345 --force
277
305
  ```
278
306
 
307
+ #### pre-sync hook
308
+
309
+ Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
310
+ `.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
311
+ changelists is given, the pre-sync hooks run once, before the first sync.
312
+
279
313
  #### post-sync hook
280
314
 
281
315
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
@@ -535,6 +569,13 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
535
569
 
536
570
  Non-executable or unrecognized hook files are skipped with a warning.
537
571
 
572
+ ### pre-sync hook
573
+
574
+ Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
575
+ `.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
576
+ changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
577
+ before the first sync.
578
+
538
579
  ### post-sync hook
539
580
 
540
581
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
@@ -133,41 +133,37 @@ cloning.
133
133
 
134
134
  These steps set up git-p4son in an existing Perforce workspace. You only need to do this once.
135
135
 
136
- 1. **Enable clobber on your Perforce workspace.** Edit the workspace in
137
- P4V to set the clobber flag, or run `p4 client` and change `noclobber` to `clobber` in the Options line.
138
- See [Why clobber?](#why-clobber) for rationale.
139
-
140
- 2. **Set a git editor** if you don't have one already. The `review` command opens an interactive rebase in your
136
+ 1. **Set a git editor** if you don't have one already. The `review` command opens an interactive rebase in your
141
137
  editor. If you haven't configured one, set it with:
142
138
  ```sh
143
139
  git config --global core.editor <editor> # e.g. vim, nano, code --wait
144
140
  ```
145
141
 
146
- 3. **Sync your workspace to a known changelist.** Pick a changelist to use as the starting point for your git
142
+ 2. **Sync your workspace to a known changelist.** Pick a changelist to use as the starting point for your git
147
143
  history:
148
144
  ```sh
149
145
  p4 sync //...@12345
150
146
  ```
151
147
 
152
- 4. **Run `git p4son init`.** This can be anywhere inside your Perforce workspace — it doesn't have to be at the
148
+ 3. **Run `git p4son init`.** This can be anywhere inside your Perforce workspace - it doesn't have to be at the
153
149
  root:
154
150
  ```sh
155
151
  cd /path/to/your/workspace # or a subdirectory of it
156
152
  git p4son init
157
153
  ```
158
- The command verifies that you are inside a P4 workspace with clobber enabled, prompts you to select a depot
154
+ The command verifies that you are inside a P4 workspace, prompts you to select a depot
159
155
  root (entire workspace or current directory subtree), runs `git init`, sets up `.gitignore` (copying from
160
156
  `.p4ignore` if available), and creates an initial commit.
161
157
 
162
- 5. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded.
158
+ 4. **Review `.gitignore`.** Edit the file to ensure build artifacts and other unwanted files are excluded.
163
159
 
164
- 6. **Add and commit all files manually the first time**
160
+ 5. **Add and commit all files manually the first time**
165
161
  ```sh
166
162
  git add .
167
163
  git commit -m "Initial submit all files"
168
164
  ```
169
165
 
170
- 7. **Run `git p4son sync`** to get your first official sync commit
166
+ 6. **Run `git p4son sync`** to get your first official sync commit
171
167
  ```sh
172
168
  git p4son sync
173
169
  ```
@@ -176,19 +172,41 @@ These steps set up git-p4son in an existing Perforce workspace. You only need to
176
172
  From here, branch off `main` for local development. See the [Usage Example](#usage-example) for a typical
177
173
  workflow.
178
174
 
179
- ### Why clobber?
175
+ ### How sync handles writable files
176
+
177
+ When you switch branches with `git checkout`, git removes the read-only flag on every file it writes. This means
178
+ that when you switch back to `main` for a sync, Perforce may report "Can't clobber writable file" errors. git-p4son
179
+ handles these automatically by classifying each writable file:
180
+
181
+ **writable-unchanged** - The file on disk is identical to the last sync (only the read-only flag was removed
182
+ by git). This is the common case after switching branches. git-p4son detects this by comparing git blob IDs against
183
+ the last sync commit that touched the file, restores the read-only flag, and lets the sync overwrite the file.
184
+
185
+ **writable-changed** - The file on disk differs from the last sync. This happens when you manually edit a file
186
+ on `main` and commit it to git without submitting to Perforce. git-p4son lets the sync overwrite the file and
187
+ includes it in the sync commit (so the commit reflects pure Perforce state), then three-way merges your local
188
+ changes on top of the new Perforce content - similar to a `git rebase`. If the merge is clean, the result appears
189
+ as an unstaged change. If there are conflicts, standard conflict markers are inserted for you to resolve.
190
+
191
+ **added both locally and in Perforce** - A file you committed to git is also being added by Perforce (p4 has never
192
+ synced it to your workspace). There is no common baseline, so git-p4son merges the two versions against an empty
193
+ base: identical content merges cleanly, otherwise the file gets a single conflict block containing both full
194
+ versions for you to resolve.
195
+
196
+ **binary files** - Binary files that have local changes cannot be three-way merged. git-p4son restores your local
197
+ version to disk after the sync commit so it shows up in `git status` for you to handle manually.
180
198
 
181
- When you switch branches with `git checkout`, git overwrites the files that differ between the source and target branch.
182
- In doing so, it always removes the read-only flag - this is just how git works.
199
+ **added locally, deleted upstream** - A file with local changes in git was deleted in Perforce. git-p4son lets the
200
+ delete stand and warns you - your version remains available in git history.
183
201
 
184
- Now imagine one of those files has also been modified in Perforce. The next time `p4 sync` runs, Perforce will complain
185
- that a file was changed outside its control. At that point you have two options: either force-sync the individual file
186
- with `p4 sync -f //path/to/file`, or enable the `clobber` flag on your workspace so that Perforce silently overwrites
187
- writable files.
202
+ **deleted locally, added upstream** - A file you deleted from git was modified in Perforce. The Perforce version is
203
+ kept in the sync commit. You can delete it again if you want.
188
204
 
189
- This is not specific to git-p4son - any workflow that combines git and Perforce in the same workspace will run into it.
190
- And in practice it is harmless: your local changes live on feature branches in git, so when you rebase onto `main` after
191
- a sync, your changes are reapplied on top of the latest Perforce state.
205
+ **ignored** - Files ignored by `.gitignore` are skipped with a warning. These are outside git's domain and should
206
+ be managed manually or via Perforce directly.
207
+
208
+ After syncing, git-p4son prints a summary of how each writable file was handled, and instructs you to review and
209
+ commit any files that need attention.
192
210
 
193
211
  ## Usage
194
212
 
@@ -218,13 +236,18 @@ Initialize a git repository inside a Perforce workspace:
218
236
  git p4son init
219
237
  ```
220
238
 
221
- This command checks preconditions (Perforce workspace, clobber flag), configures the depot root, runs `git init`,
239
+ This command checks preconditions (Perforce workspace), configures the depot root, runs `git init`,
222
240
  sets up `.gitignore`, and creates an initial commit.
223
241
 
224
242
  The depot root determines which part of the Perforce workspace git-p4son syncs. You can choose to sync the entire
225
243
  workspace or just the directory's subtree where the git root is placed. The selection is saved in
226
244
  `.git-p4son/config.toml` and used by all subsequent commands.
227
245
 
246
+ The saved root uses a `$(workspace)` placeholder for the workspace (client) name, e.g.
247
+ `root = "//$(workspace)/Engine"`. git-p4son substitutes the live workspace name each time it runs, so renaming
248
+ the Perforce workspace does not break the config. You can still store a concrete name (e.g. `//my-workspace/Engine`)
249
+ by hand-editing `config.toml`; both forms work.
250
+
228
251
  The `.gitignore` is set up using this priority:
229
252
  - If `.gitignore` already exists, it is left as is
230
253
  - If `.p4ignore` exists, it is copied to `.gitignore` as a starting point
@@ -235,24 +258,35 @@ The `.gitignore` is set up using this priority:
235
258
  Sync local git repository with a Perforce workspace:
236
259
 
237
260
  ```sh
238
- git p4son sync [changelist] [--force]
261
+ git p4son sync [changelist ...] [--force]
239
262
  ```
240
263
 
241
264
  **Arguments:**
242
- - `changelist` (optional): Changelist number, or `last-synced` to re-sync the last synced changelist. Omit to
243
- sync to the latest changelist affecting the workspace.
265
+ - `changelist` (optional): Changelist number, `last-synced` to re-sync the last synced changelist, or `head`
266
+ to sync to the latest changelist. Omit to sync to the latest changelist affecting the workspace. Several
267
+ strictly increasing changelist numbers may be given to sync each in sequence, producing one commit per
268
+ changelist. A trailing `head` may close out such a sequence to finish at the latest changelist.
244
269
 
245
270
  **Options:**
246
- - `-f, --force`: Force sync encountered writable files and allow syncing to older changelists.
271
+ - `-f, --force`: Allow syncing to changelists older than the current one.
247
272
 
248
273
  **Examples:**
249
274
  ```sh
250
275
  git p4son sync # sync to latest
276
+ git p4son sync head # sync to latest (explicit)
251
277
  git p4son sync 12345
278
+ git p4son sync 123 156 178 # sync each changelist in sequence, one commit each
279
+ git p4son sync 123 156 head # sync 123, 156, then the latest changelist
252
280
  git p4son sync last-synced
253
281
  git p4son sync 12345 --force
254
282
  ```
255
283
 
284
+ #### pre-sync hook
285
+
286
+ Before `git-p4son sync` syncs any changelist, git-p4son runs [hooks](#hooks) from
287
+ `.git-p4son/hooks/pre-sync/`. If any hook exits non-zero the sync is aborted. When a sequence of
288
+ changelists is given, the pre-sync hooks run once, before the first sync.
289
+
256
290
  #### post-sync hook
257
291
 
258
292
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs [hooks](#hooks)
@@ -512,6 +546,13 @@ On Windows, `.ps1`, `.nu`, `.sh`, and `.py` files are run with `powershell.exe`,
512
546
 
513
547
  Non-executable or unrecognized hook files are skipped with a warning.
514
548
 
549
+ ### pre-sync hook
550
+
551
+ Before `git-p4son sync` syncs any changelist, git-p4son runs executable hooks from
552
+ `.git-p4son/hooks/pre-sync/`. If any hook exits with a non-zero code the sync is aborted before any
553
+ changelist is synced. When a sequence of changelists is synced, the pre-sync hooks run only once,
554
+ before the first sync.
555
+
515
556
  ### post-sync hook
516
557
 
517
558
  After a successful `git-p4son sync` that actually performs sync work, git-p4son runs executable hooks from
@@ -7,6 +7,6 @@ creating and updating changelists, and managing Swarm reviews.
7
7
 
8
8
  CONFIG_DIR = '.git-p4son'
9
9
 
10
- __version__ = "0.2.11"
10
+ __version__ = "0.3.1"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -8,6 +8,7 @@ from .changelist_store import (
8
8
  list_changelist_aliases,
9
9
  delete_changelist_alias,
10
10
  )
11
+ from .common import prompt_choice
11
12
  from .log import log
12
13
 
13
14
 
@@ -54,31 +55,6 @@ def alias_delete_command(args: argparse.Namespace) -> int:
54
55
  return 0
55
56
 
56
57
 
57
- def _prompt_choice(prefix: str, options: list[str]) -> str | None:
58
- """Prompt until the response matches an option or its first letter.
59
-
60
- The choices are rendered from the options, e.g. "[y]es / [n]o", so the
61
- prompt and the accepted keys cannot drift apart. Options must have
62
- unique first letters. Returns the chosen option, or None on EOF.
63
- """
64
- shorthands = {option[0]: option for option in options}
65
- rendered = ' / '.join(f'[{option[0]}]{option[1:]}' for option in options)
66
- keys = list(shorthands)
67
- while True:
68
- try:
69
- response = input(f'{prefix} {rendered}: ').strip().lower()
70
- except EOFError:
71
- print()
72
- return None
73
-
74
- if response in shorthands:
75
- return shorthands[response]
76
- elif response in options:
77
- return response
78
- else:
79
- print('Please enter ' + ', '.join(keys[:-1]) + f' or {keys[-1]}')
80
-
81
-
82
58
  def _clean_all(aliases: list[tuple[str, str]], workspace_dir: str) -> None:
83
59
  """Delete every alias without further prompting."""
84
60
  for name, _changelist in aliases:
@@ -98,7 +74,7 @@ def _clean_interactive(aliases: list[tuple[str, str]],
98
74
  log.heading(f'{name} -> CL {changelist}')
99
75
 
100
76
  if response != 'all':
101
- response = _prompt_choice(
77
+ response = prompt_choice(
102
78
  'Delete?', ['yes', 'no', 'all', 'quit'])
103
79
 
104
80
  if response is None or response == 'quit':
@@ -125,7 +101,7 @@ def alias_clean_command(args: argparse.Namespace) -> int:
125
101
  for name, changelist in aliases:
126
102
  log.info(f'{name} -> {changelist}')
127
103
 
128
- mode = _prompt_choice('Delete?', ['all', 'interactive', 'quit'])
104
+ mode = prompt_choice('Delete?', ['all', 'interactive', 'quit'])
129
105
  if mode is None or mode == 'quit':
130
106
  log.info('Aborting')
131
107
  return 0
@@ -11,7 +11,7 @@ from . import CONFIG_DIR
11
11
  from .log import log
12
12
 
13
13
 
14
- RESERVED_KEYWORDS = frozenset({'last-synced', 'branch'})
14
+ RESERVED_KEYWORDS = frozenset({'last-synced', 'head', 'branch'})
15
15
 
16
16
  # Allowed characters: ASCII letters, digits, hyphen, underscore, dot.
17
17
  # Must not start or end with a dot, so "." and ".." are rejected and the
@@ -31,7 +31,10 @@ def create_parser() -> argparse.ArgumentParser:
31
31
  epilog="""
32
32
  Examples:
33
33
  git-p4son sync # Sync to the latest changelist
34
+ git-p4son sync head # Sync to the latest changelist (explicit)
34
35
  git-p4son sync 12345 # Sync to changelist 12345
36
+ git-p4son sync 123 156 178 # Sync each changelist in sequence, one commit each
37
+ git-p4son sync 123 156 head # Sync 123, 156, then the latest changelist
35
38
  git-p4son sync last-synced # Re-sync the last synced changelist
36
39
  git-p4son new -m "Fix bug" # Create changelist, alias defaults to branch name
37
40
  git-p4son new -m "Fix bug" --review # Create changelist, create Swarm review
@@ -66,7 +69,7 @@ Examples:
66
69
  'init',
67
70
  help='Initialize a git repository inside a Perforce workspace',
68
71
  description='Set up a new git repository inside a Perforce workspace. '
69
- 'Checks preconditions (P4 workspace, clobber flag), initializes git, '
72
+ 'Checks preconditions (P4 workspace), initializes git, '
70
73
  'sets up .gitignore, and creates an initial commit.'
71
74
  )
72
75
 
@@ -78,18 +81,19 @@ Examples:
78
81
  )
79
82
  sync_parser.add_argument(
80
83
  'changelist',
81
- nargs='?',
82
- default=None,
83
- help='Changelist number to sync, or "last-synced" to re-sync the last synced changelist. '
84
- 'Omit to sync to the latest changelist affecting the workspace'
84
+ nargs='*',
85
+ default=[],
86
+ metavar='CHANGELIST',
87
+ help='Changelist number(s) to sync, "last-synced" to re-sync the last synced changelist, '
88
+ 'or "head" to sync to the latest changelist. Give several strictly increasing '
89
+ 'changelist numbers to sync each in sequence, one commit per changelist; "head" may '
90
+ 'close out such a sequence as a trailing target. Omit to sync to the latest '
91
+ 'changelist affecting the workspace'
85
92
  )
86
93
  sync_parser.add_argument(
87
94
  '-f', '--force',
88
95
  action='store_true',
89
- help='Force sync encountered writable files and allow syncing to older changelists. '
90
- 'When clobber is not enabled on your workspace, p4 will fail to sync files that '
91
- 'are read-only. git removes the readonly flag on touched files. Also allows '
92
- 'syncing to changelists older than the current one.'
96
+ help='Allow syncing to changelists older than the current one.'
93
97
  )
94
98
 
95
99
  # New subcommand
@@ -36,6 +36,31 @@ def branch_to_alias(branch_name: str) -> str:
36
36
  return branch_name.replace('/', '-')
37
37
 
38
38
 
39
+ def prompt_choice(prefix: str, options: list[str]) -> str | None:
40
+ """Prompt until the response matches an option or its first letter.
41
+
42
+ The choices are rendered from the options, e.g. "[y]es / [n]o", so the
43
+ prompt and the accepted keys cannot drift apart. Options must have
44
+ unique first letters. Returns the chosen option, or None on EOF.
45
+ """
46
+ shorthands = {option[0]: option for option in options}
47
+ rendered = ' / '.join(f'[{option[0]}]{option[1:]}' for option in options)
48
+ keys = list(shorthands)
49
+ while True:
50
+ try:
51
+ response = input(f'{prefix} {rendered}: ').strip().lower()
52
+ except EOFError:
53
+ print()
54
+ return None
55
+
56
+ if response in shorthands:
57
+ return shorthands[response]
58
+ elif response in options:
59
+ return response
60
+ else:
61
+ print('Please enter ' + ', '.join(keys[:-1]) + f' or {keys[-1]}')
62
+
63
+
39
64
  def _path_module_for(*paths: str):
40
65
  """Choose a path module that matches the given path strings."""
41
66
  if any('\\' in path or re.match(r'^[A-Za-z]:', path) for path in paths):
@@ -106,11 +106,16 @@ def _complete_positional(command, subcommand, positional_count,
106
106
  """Complete a positional argument."""
107
107
  aliases = _get_alias_names(workspace_dir)
108
108
 
109
- if command == 'sync' and positional_count == 0:
110
- candidates = [
111
- ('last-synced', 'Re-sync last synced changelist'),
112
- ]
113
- return _filter(candidates, prefix)
109
+ if command == 'sync':
110
+ if positional_count == 0:
111
+ candidates = [
112
+ ('head', 'Sync to the latest changelist'),
113
+ ('last-synced', 'Re-sync last synced changelist'),
114
+ ]
115
+ return _filter(candidates, prefix)
116
+ # "head" may also close out a sequence of changelist numbers.
117
+ return _filter(
118
+ [('head', 'Sync to the latest changelist')], prefix)
114
119
 
115
120
  if command == 'update' and positional_count == 0:
116
121
  branch_candidates = _get_branch_candidates(prefix, workspace_dir)
@@ -12,21 +12,40 @@ from . import CONFIG_DIR
12
12
 
13
13
  _BARE_KEY_RE = re.compile(r'^[A-Za-z0-9_-]+$')
14
14
 
15
+ # Placeholder allowed in a stored depot root, substituted with the live
16
+ # Perforce client (workspace) name each time the root is used. Storing e.g.
17
+ # root = "//$(workspace)/Engine" keeps the config working after the workspace
18
+ # is renamed, at the cost of one client-name lookup per command.
19
+ WORKSPACE_PLACEHOLDER = '$(workspace)'
20
+
15
21
 
16
22
  def config_path(workspace_dir: str) -> str:
17
23
  """Return the path to the config file."""
18
24
  return os.path.join(workspace_dir, CONFIG_DIR, 'config.toml')
19
25
 
20
26
 
21
- def load_config(workspace_dir: str) -> dict:
22
- """Read and parse the config file. Returns empty dict if missing."""
23
- path = config_path(workspace_dir)
27
+ def load_toml(path: str) -> dict:
28
+ """Read and parse a TOML file. Returns empty dict if missing."""
24
29
  if not os.path.exists(path):
25
30
  return {}
26
31
  with open(path, 'rb') as f:
27
32
  return tomllib.load(f)
28
33
 
29
34
 
35
+ def write_toml(path: str, data: dict) -> None:
36
+ """Write data to a TOML file, creating parent directories as needed."""
37
+ os.makedirs(os.path.dirname(path), exist_ok=True)
38
+ # tomllib reads strict UTF-8, so write UTF-8 explicitly.
39
+ with open(path, 'w', encoding='utf-8') as f:
40
+ for section, values in data.items():
41
+ _write_table(f, _format_key(section), values)
42
+
43
+
44
+ def load_config(workspace_dir: str) -> dict:
45
+ """Read and parse the config file. Returns empty dict if missing."""
46
+ return load_toml(config_path(workspace_dir))
47
+
48
+
30
49
  def save_config(workspace_dir: str, config: dict) -> None:
31
50
  """Merge config into the config file, section by section.
32
51
 
@@ -36,13 +55,7 @@ def save_config(workspace_dir: str, config: dict) -> None:
36
55
  merged = load_config(workspace_dir)
37
56
  for section, values in config.items():
38
57
  merged.setdefault(section, {}).update(values)
39
-
40
- path = config_path(workspace_dir)
41
- os.makedirs(os.path.dirname(path), exist_ok=True)
42
- # tomllib reads strict UTF-8, so write UTF-8 explicitly.
43
- with open(path, 'w', encoding='utf-8') as f:
44
- for section, values in merged.items():
45
- _write_table(f, _format_key(section), values)
58
+ write_toml(config_path(workspace_dir), merged)
46
59
 
47
60
 
48
61
  def _format_string(value: str) -> str:
@@ -87,3 +100,8 @@ def get_depot_root(workspace_dir: str) -> str | None:
87
100
  """Get the depot root from config, or None if not configured."""
88
101
  config = load_config(workspace_dir)
89
102
  return config.get('depot', {}).get('root')
103
+
104
+
105
+ def expand_depot_root(depot_root: str, workspace_name: str) -> str:
106
+ """Substitute the live workspace name for the $(workspace) placeholder."""
107
+ return depot_root.replace(WORKSPACE_PLACEHOLDER, workspace_name)