git-p4son 0.2.6__tar.gz → 0.2.8__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 (44) hide show
  1. {git_p4son-0.2.6 → git_p4son-0.2.8}/PKG-INFO +1 -1
  2. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/__init__.py +1 -1
  3. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/perforce.py +38 -9
  4. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son.egg-info/PKG-INFO +1 -1
  5. {git_p4son-0.2.6 → git_p4son-0.2.8}/pyproject.toml +1 -1
  6. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_lib_edit.py +110 -13
  7. {git_p4son-0.2.6 → git_p4son-0.2.8}/LICENSE +0 -0
  8. {git_p4son-0.2.6 → git_p4son-0.2.8}/README.md +0 -0
  9. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/__main__.py +0 -0
  10. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/alias.py +0 -0
  11. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/changelist_store.py +0 -0
  12. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/cli.py +0 -0
  13. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/common.py +0 -0
  14. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/complete.py +0 -0
  15. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/completions/_git-p4son +0 -0
  16. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/completions/git-p4son.bash +0 -0
  17. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/completions/git-p4son.ps1 +0 -0
  18. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/config.py +0 -0
  19. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/git.py +0 -0
  20. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/init.py +0 -0
  21. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/lib.py +0 -0
  22. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/list_changes.py +0 -0
  23. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/log.py +0 -0
  24. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/new.py +0 -0
  25. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/review.py +0 -0
  26. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/sync.py +0 -0
  27. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son/update.py +0 -0
  28. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son.egg-info/SOURCES.txt +0 -0
  29. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son.egg-info/dependency_links.txt +0 -0
  30. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son.egg-info/entry_points.txt +0 -0
  31. {git_p4son-0.2.6 → git_p4son-0.2.8}/git_p4son.egg-info/top_level.txt +0 -0
  32. {git_p4son-0.2.6 → git_p4son-0.2.8}/setup.cfg +0 -0
  33. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_cli.py +0 -0
  34. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_common.py +0 -0
  35. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_complete.py +0 -0
  36. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_config.py +0 -0
  37. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_init.py +0 -0
  38. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_lib_changelist.py +0 -0
  39. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_lib_review.py +0 -0
  40. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_list_changes.py +0 -0
  41. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_log.py +0 -0
  42. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_perforce.py +0 -0
  43. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_review.py +0 -0
  44. {git_p4son-0.2.6 → git_p4son-0.2.8}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -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.6"
10
+ __version__ = "0.2.8"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -177,26 +177,55 @@ def get_latest_changelist(depot_root: str, workspace_dir: str) -> int:
177
177
 
178
178
  # --- file operations ---
179
179
 
180
- def get_changelist_for_file(filename: str, workspace_dir: str) -> str | None:
181
- """Return the changelist a file is opened in, or None if not opened."""
180
+ def get_changelist_for_file(filename: str, workspace_dir: str) -> tuple[str, str] | None:
181
+ """Return (changelist, action) for an opened file, or None if not opened."""
182
182
  res = run(['p4', '-ztag', 'opened', filename], cwd=workspace_dir)
183
183
  fields = parse_ztag_output(res.stdout)
184
- return fields.get('change')
184
+ change = fields.get('change')
185
+ if change is None:
186
+ return None
187
+ return (change, fields.get('action', ''))
185
188
 
186
189
 
187
190
  def _ensure_in_changelist(filename: str, p4_action: str, changelist: str,
188
191
  workspace_dir: str, dry_run: bool) -> None:
189
- """Ensure a file is opened in the given changelist.
192
+ """Ensure a file is opened with the correct action in the given changelist.
190
193
 
191
194
  If the file is not yet opened, run the specified p4 action (add, edit, delete).
192
- If it's already opened in a different changelist, reopen it.
193
- If it's already in the correct changelist, do nothing.
195
+ If it's already opened with a different action, revert and reopen.
196
+ If it's already opened with the correct action in a different changelist, reopen it.
197
+ If it's already in the correct changelist with the correct action, do nothing.
194
198
  """
195
- current = get_changelist_for_file(filename, workspace_dir)
196
- if current is None:
199
+ result = get_changelist_for_file(filename, workspace_dir)
200
+ if result is None:
201
+ run(['p4', p4_action, '-c', changelist, filename],
202
+ cwd=workspace_dir, dry_run=dry_run)
203
+ return
204
+
205
+ current_cl, current_action = result
206
+ if current_action != p4_action:
207
+ # Action mismatch - revert first, then reopen with correct action.
208
+ # p4 revert overwrites the file on disk with the depot version,
209
+ # so we need git restore afterwards to get the git content back.
210
+
211
+ # add -> edit: the file is new to the depot, so it must stay as add.
212
+ # This happens when a file is added in one commit and modified in the next.
213
+ if current_action == 'add' and p4_action == 'edit':
214
+ if current_cl != changelist:
215
+ run(['p4', 'reopen', '-c', changelist, filename],
216
+ cwd=workspace_dir, dry_run=dry_run)
217
+ return
218
+
219
+ run(['p4', 'revert', filename], cwd=workspace_dir, dry_run=dry_run)
220
+ # For add -> delete: the file never existed in the depot, so just revert.
221
+ if current_action == 'add' and p4_action == 'delete':
222
+ return
197
223
  run(['p4', p4_action, '-c', changelist, filename],
198
224
  cwd=workspace_dir, dry_run=dry_run)
199
- elif current != changelist:
225
+ if p4_action != 'delete':
226
+ run(['git', 'restore', filename],
227
+ cwd=workspace_dir, dry_run=dry_run)
228
+ elif current_cl != changelist:
200
229
  run(['p4', 'reopen', '-c', changelist, filename],
201
230
  cwd=workspace_dir, dry_run=dry_run)
202
231
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-p4son"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "Utility for keeping a Perforce workspace and local git repo in sync"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -33,7 +33,7 @@ class TestGetChangelistForFile(unittest.TestCase):
33
33
  '... type text',
34
34
  ])
35
35
  result = get_changelist_for_file('foo.txt', '/ws')
36
- self.assertEqual(result, 'default')
36
+ self.assertEqual(result, ('default', 'edit'))
37
37
 
38
38
  @mock.patch('git_p4son.perforce.run')
39
39
  def test_file_in_numbered_changelist(self, mock_run):
@@ -44,7 +44,7 @@ class TestGetChangelistForFile(unittest.TestCase):
44
44
  '... type text',
45
45
  ])
46
46
  result = get_changelist_for_file('foo.txt', '/ws')
47
- self.assertEqual(result, '12345')
47
+ self.assertEqual(result, ('12345', 'edit'))
48
48
 
49
49
  @mock.patch('git_p4son.perforce.run')
50
50
  def test_file_opened_for_add(self, mock_run):
@@ -54,7 +54,7 @@ class TestGetChangelistForFile(unittest.TestCase):
54
54
  '... change 12345',
55
55
  ])
56
56
  result = get_changelist_for_file('foo.txt', '/ws')
57
- self.assertEqual(result, '12345')
57
+ self.assertEqual(result, ('12345', 'add'))
58
58
 
59
59
  @mock.patch('git_p4son.perforce.run')
60
60
  def test_file_opened_for_delete(self, mock_run):
@@ -64,7 +64,7 @@ class TestGetChangelistForFile(unittest.TestCase):
64
64
  '... change 12345',
65
65
  ])
66
66
  result = get_changelist_for_file('foo.txt', '/ws')
67
- self.assertEqual(result, '12345')
67
+ self.assertEqual(result, ('12345', 'delete'))
68
68
 
69
69
  @mock.patch('git_p4son.perforce.run')
70
70
  def test_file_opened_for_move_add(self, mock_run):
@@ -74,7 +74,7 @@ class TestGetChangelistForFile(unittest.TestCase):
74
74
  '... change 12345',
75
75
  ])
76
76
  result = get_changelist_for_file('foo.txt', '/ws')
77
- self.assertEqual(result, '12345')
77
+ self.assertEqual(result, ('12345', 'move/add'))
78
78
 
79
79
  @mock.patch('git_p4son.perforce.run')
80
80
  def test_add_in_default_changelist(self, mock_run):
@@ -84,7 +84,7 @@ class TestGetChangelistForFile(unittest.TestCase):
84
84
  '... change default',
85
85
  ])
86
86
  result = get_changelist_for_file('foo.txt', '/ws')
87
- self.assertEqual(result, 'default')
87
+ self.assertEqual(result, ('default', 'add'))
88
88
 
89
89
 
90
90
  class TestFindCommonAncestor(unittest.TestCase):
@@ -157,7 +157,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
157
157
  cwd='/ws', dry_run=False,
158
158
  )
159
159
 
160
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='200')
160
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'add'))
161
161
  @mock.patch('git_p4son.perforce.run')
162
162
  def test_reopens_added_file_in_different_changelist(self, mock_run, mock_check):
163
163
  mock_run.return_value = make_run_result()
@@ -169,7 +169,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
169
169
  cwd='/ws', dry_run=False,
170
170
  )
171
171
 
172
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='100')
172
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'add'))
173
173
  @mock.patch('git_p4son.perforce.run')
174
174
  def test_skips_added_file_already_in_correct_changelist(self, mock_run, mock_check):
175
175
  mock_run.return_value = make_run_result()
@@ -191,7 +191,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
191
191
  cwd='/ws', dry_run=False,
192
192
  )
193
193
 
194
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='200')
194
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'edit'))
195
195
  @mock.patch('git_p4son.perforce.run')
196
196
  def test_reopens_file_in_different_changelist(self, mock_run, mock_check):
197
197
  mock_run.return_value = make_run_result()
@@ -203,7 +203,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
203
203
  cwd='/ws', dry_run=False,
204
204
  )
205
205
 
206
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='100')
206
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'edit'))
207
207
  @mock.patch('git_p4son.perforce.run')
208
208
  def test_skips_file_already_in_correct_changelist(self, mock_run, mock_check):
209
209
  mock_run.return_value = make_run_result()
@@ -225,7 +225,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
225
225
  cwd='/ws', dry_run=False,
226
226
  )
227
227
 
228
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='200')
228
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'delete'))
229
229
  @mock.patch('git_p4son.perforce.run')
230
230
  def test_reopens_deleted_file_in_different_changelist(self, mock_run, mock_check):
231
231
  mock_run.return_value = make_run_result()
@@ -237,7 +237,7 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
237
237
  cwd='/ws', dry_run=False,
238
238
  )
239
239
 
240
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='100')
240
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'delete'))
241
241
  @mock.patch('git_p4son.perforce.run')
242
242
  def test_skips_deleted_file_already_in_correct_changelist(self, mock_run, mock_check):
243
243
  mock_run.return_value = make_run_result()
@@ -260,7 +260,8 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
260
260
  'p4', 'delete', '-c', '100', 'old.txt'])
261
261
  self.assertEqual(calls[1][0][0], ['p4', 'add', '-c', '100', 'new.txt'])
262
262
 
263
- @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value='200')
263
+ @mock.patch('git_p4son.perforce.get_changelist_for_file',
264
+ side_effect=[('200', 'delete'), ('200', 'add')])
264
265
  @mock.patch('git_p4son.perforce.run')
265
266
  def test_reopens_moved_files_in_different_changelist(self, mock_run, mock_check):
266
267
  mock_run.return_value = make_run_result()
@@ -288,6 +289,102 @@ class TestIncludeChangesInChangelist(unittest.TestCase):
288
289
  )
289
290
 
290
291
 
292
+ class TestActionMismatch(unittest.TestCase):
293
+ """Tests for reopening files when the p4 action doesn't match the desired action."""
294
+
295
+ # --- edit -> delete ---
296
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'edit'))
297
+ @mock.patch('git_p4son.perforce.run')
298
+ def test_edit_to_delete_same_cl(self, mock_run, mock_check):
299
+ mock_run.return_value = make_run_result()
300
+ changes = LocalChanges()
301
+ changes.dels = ['file.txt']
302
+ include_changes_in_changelist(changes, '100', '/ws')
303
+ calls = mock_run.call_args_list
304
+ self.assertEqual(len(calls), 2)
305
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
306
+ self.assertEqual(calls[1][0][0],
307
+ ['p4', 'delete', '-c', '100', 'file.txt'])
308
+
309
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'edit'))
310
+ @mock.patch('git_p4son.perforce.run')
311
+ def test_edit_to_delete_different_cl(self, mock_run, mock_check):
312
+ mock_run.return_value = make_run_result()
313
+ changes = LocalChanges()
314
+ changes.dels = ['file.txt']
315
+ include_changes_in_changelist(changes, '100', '/ws')
316
+ calls = mock_run.call_args_list
317
+ self.assertEqual(len(calls), 2)
318
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
319
+ self.assertEqual(calls[1][0][0],
320
+ ['p4', 'delete', '-c', '100', 'file.txt'])
321
+
322
+ # --- delete -> edit ---
323
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'delete'))
324
+ @mock.patch('git_p4son.perforce.run')
325
+ def test_delete_to_edit_same_cl(self, mock_run, mock_check):
326
+ mock_run.return_value = make_run_result()
327
+ changes = LocalChanges()
328
+ changes.mods = ['file.txt']
329
+ include_changes_in_changelist(changes, '100', '/ws')
330
+ calls = mock_run.call_args_list
331
+ self.assertEqual(len(calls), 3)
332
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
333
+ self.assertEqual(calls[1][0][0],
334
+ ['p4', 'edit', '-c', '100', 'file.txt'])
335
+ self.assertEqual(calls[2][0][0],
336
+ ['git', 'restore', 'file.txt'])
337
+
338
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'delete'))
339
+ @mock.patch('git_p4son.perforce.run')
340
+ def test_delete_to_edit_different_cl(self, mock_run, mock_check):
341
+ mock_run.return_value = make_run_result()
342
+ changes = LocalChanges()
343
+ changes.mods = ['file.txt']
344
+ include_changes_in_changelist(changes, '100', '/ws')
345
+ calls = mock_run.call_args_list
346
+ self.assertEqual(len(calls), 3)
347
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
348
+ self.assertEqual(calls[1][0][0],
349
+ ['p4', 'edit', '-c', '100', 'file.txt'])
350
+ self.assertEqual(calls[2][0][0],
351
+ ['git', 'restore', 'file.txt'])
352
+
353
+ # --- add -> edit (keep as add, file is new to depot) ---
354
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'add'))
355
+ @mock.patch('git_p4son.perforce.run')
356
+ def test_add_to_edit_same_cl_keeps_add(self, mock_run, mock_check):
357
+ mock_run.return_value = make_run_result()
358
+ changes = LocalChanges()
359
+ changes.mods = ['file.txt']
360
+ include_changes_in_changelist(changes, '100', '/ws')
361
+ mock_run.assert_not_called()
362
+
363
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('200', 'add'))
364
+ @mock.patch('git_p4son.perforce.run')
365
+ def test_add_to_edit_different_cl_reopens(self, mock_run, mock_check):
366
+ mock_run.return_value = make_run_result()
367
+ changes = LocalChanges()
368
+ changes.mods = ['file.txt']
369
+ include_changes_in_changelist(changes, '100', '/ws')
370
+ mock_run.assert_called_once_with(
371
+ ['p4', 'reopen', '-c', '100', 'file.txt'],
372
+ cwd='/ws', dry_run=False,
373
+ )
374
+
375
+ # --- add -> delete (revert only, no reopen) ---
376
+ @mock.patch('git_p4son.perforce.get_changelist_for_file', return_value=('100', 'add'))
377
+ @mock.patch('git_p4son.perforce.run')
378
+ def test_add_to_delete_reverts_only(self, mock_run, mock_check):
379
+ mock_run.return_value = make_run_result()
380
+ changes = LocalChanges()
381
+ changes.dels = ['file.txt']
382
+ include_changes_in_changelist(changes, '100', '/ws')
383
+ calls = mock_run.call_args_list
384
+ self.assertEqual(len(calls), 1)
385
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
386
+
387
+
291
388
  class TestOpenChangesForEdit(unittest.TestCase):
292
389
  @mock.patch('git_p4son.lib.include_changes_in_changelist')
293
390
  @mock.patch('git_p4son.lib.get_local_changes')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes