git-p4son 0.2.5__tar.gz → 0.2.7__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.5 → git_p4son-0.2.7}/PKG-INFO +6 -6
  2. {git_p4son-0.2.5 → git_p4son-0.2.7}/README.md +5 -5
  3. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/__init__.py +1 -1
  4. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/alias.py +39 -45
  5. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/cli.py +8 -8
  6. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/complete.py +1 -1
  7. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/perforce.py +29 -9
  8. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son.egg-info/PKG-INFO +6 -6
  9. {git_p4son-0.2.5 → git_p4son-0.2.7}/pyproject.toml +1 -1
  10. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_complete.py +11 -11
  11. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_lib_edit.py +88 -13
  12. {git_p4son-0.2.5 → git_p4son-0.2.7}/LICENSE +0 -0
  13. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/__main__.py +0 -0
  14. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/changelist_store.py +0 -0
  15. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/common.py +0 -0
  16. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/completions/_git-p4son +0 -0
  17. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/completions/git-p4son.bash +0 -0
  18. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/completions/git-p4son.ps1 +0 -0
  19. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/config.py +0 -0
  20. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/git.py +0 -0
  21. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/init.py +0 -0
  22. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/lib.py +0 -0
  23. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/list_changes.py +0 -0
  24. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/log.py +0 -0
  25. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/new.py +0 -0
  26. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/review.py +0 -0
  27. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/sync.py +0 -0
  28. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son/update.py +0 -0
  29. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son.egg-info/SOURCES.txt +0 -0
  30. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son.egg-info/dependency_links.txt +0 -0
  31. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son.egg-info/entry_points.txt +0 -0
  32. {git_p4son-0.2.5 → git_p4son-0.2.7}/git_p4son.egg-info/top_level.txt +0 -0
  33. {git_p4son-0.2.5 → git_p4son-0.2.7}/setup.cfg +0 -0
  34. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_cli.py +0 -0
  35. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_common.py +0 -0
  36. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_config.py +0 -0
  37. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_init.py +0 -0
  38. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_lib_changelist.py +0 -0
  39. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_lib_review.py +0 -0
  40. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_list_changes.py +0 -0
  41. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_log.py +0 -0
  42. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_perforce.py +0 -0
  43. {git_p4son-0.2.5 → git_p4son-0.2.7}/tests/test_review.py +0 -0
  44. {git_p4son-0.2.5 → git_p4son-0.2.7}/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.5
3
+ Version: 0.2.7
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
@@ -418,12 +418,12 @@ git p4son alias list
418
418
  git p4son alias list
419
419
  ```
420
420
 
421
- #### alias set
421
+ #### alias new
422
422
 
423
423
  Save a changelist number under a named alias:
424
424
 
425
425
  ```sh
426
- git p4son alias set <changelist> [alias] [--force]
426
+ git p4son alias new <changelist> [alias] [--force]
427
427
  ```
428
428
 
429
429
  **Arguments:**
@@ -435,9 +435,9 @@ git p4son alias set <changelist> [alias] [--force]
435
435
 
436
436
  **Examples:**
437
437
  ```sh
438
- git p4son alias set 12345 # alias defaults to branch name
439
- git p4son alias set 12345 myfeature
440
- git p4son alias set 67890 myfeature -f
438
+ git p4son alias new 12345 # alias defaults to branch name
439
+ git p4son alias new 12345 myfeature
440
+ git p4son alias new 67890 myfeature -f
441
441
  ```
442
442
 
443
443
  #### alias delete
@@ -395,12 +395,12 @@ git p4son alias list
395
395
  git p4son alias list
396
396
  ```
397
397
 
398
- #### alias set
398
+ #### alias new
399
399
 
400
400
  Save a changelist number under a named alias:
401
401
 
402
402
  ```sh
403
- git p4son alias set <changelist> [alias] [--force]
403
+ git p4son alias new <changelist> [alias] [--force]
404
404
  ```
405
405
 
406
406
  **Arguments:**
@@ -412,9 +412,9 @@ git p4son alias set <changelist> [alias] [--force]
412
412
 
413
413
  **Examples:**
414
414
  ```sh
415
- git p4son alias set 12345 # alias defaults to branch name
416
- git p4son alias set 12345 myfeature
417
- git p4son alias set 67890 myfeature -f
415
+ git p4son alias new 12345 # alias defaults to branch name
416
+ git p4son alias new 12345 myfeature
417
+ git p4son alias new 67890 myfeature -f
418
418
  ```
419
419
 
420
420
  #### alias delete
@@ -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.5"
10
+ __version__ = "0.2.7"
11
11
  __author__ = "Andreas Andersson"
12
12
  __email__ = "andreas@neoboid.com"
@@ -19,17 +19,16 @@ def alias_list_command(args: argparse.Namespace) -> int:
19
19
  if not aliases:
20
20
  log.info('No aliases defined')
21
21
  return 0
22
+ log.info(f'Found {len(aliases)}')
22
23
 
23
24
  for name, changelist in aliases:
24
25
  log.info(f'{name} -> {changelist}')
25
26
 
26
- log.success(f'{len(aliases)} listed')
27
-
28
27
  return 0
29
28
 
30
29
 
31
- def alias_set_command(args: argparse.Namespace) -> int:
32
- """Execute the 'alias set' command."""
30
+ def alias_new_command(args: argparse.Namespace) -> int:
31
+ """Execute the 'alias new' command."""
33
32
  workspace_dir = args.workspace_dir
34
33
 
35
34
  if not args.changelist.isdigit():
@@ -55,6 +54,27 @@ def alias_delete_command(args: argparse.Namespace) -> int:
55
54
  return 0
56
55
 
57
56
 
57
+ def _prompt_delete(prompt: str) -> str | None:
58
+ """Prompt until a valid y/n/a/q response is given. Returns None on EOF."""
59
+ while True:
60
+ try:
61
+ response = input(prompt).strip().lower()
62
+ except EOFError:
63
+ print()
64
+ return None
65
+
66
+ if response in ('y', 'yes'):
67
+ return 'yes'
68
+ elif response in ('n', 'no'):
69
+ return 'no'
70
+ elif response in ('a', 'all'):
71
+ return 'all'
72
+ elif response in ('q', 'quit'):
73
+ return 'quit'
74
+ else:
75
+ print('Please enter y, n, a, or q')
76
+
77
+
58
78
  def alias_clean_command(args: argparse.Namespace) -> int:
59
79
  """Execute the 'alias clean' command with interactive prompts."""
60
80
  workspace_dir = args.workspace_dir
@@ -64,50 +84,24 @@ def alias_clean_command(args: argparse.Namespace) -> int:
64
84
  log.success('No changelist aliases to clean')
65
85
  return 0
66
86
 
67
- delete_all = False
68
- deleted_count = 0
69
-
87
+ response = None
70
88
  for name, changelist in aliases:
71
- # Interactive output stays as print() — it's a prompt-response UI
72
- print(f'{name} -> {changelist}')
89
+ log.heading(f'{name} -> CL {changelist}')
73
90
 
74
- if delete_all:
75
- delete_changelist_alias(name, workspace_dir)
76
- deleted_count += 1
77
- log.info(' Deleted')
78
- continue
91
+ if response != 'all':
92
+ response = _prompt_delete(
93
+ 'Delete? [y]es / [n]o / [a]ll / [q]uit: ')
79
94
 
80
- quit = False
81
- while True:
82
- try:
83
- response = input(
84
- 'Delete? [y]es / [n]o / [a]ll / [q]uit: ').strip().lower()
85
- except EOFError:
86
- print()
87
- return 0
88
-
89
- if response in ('y', 'yes'):
90
- delete_changelist_alias(name, workspace_dir)
91
- deleted_count += 1
92
- log.info(' Deleted')
93
- break
94
- elif response in ('n', 'no'):
95
- break
96
- elif response in ('a', 'all'):
97
- delete_all = True
98
- delete_changelist_alias(name, workspace_dir)
99
- deleted_count += 1
100
- log.info(' Deleted')
101
- break
102
- elif response in ('q', 'quit'):
103
- quit = True
104
- break
105
- else:
106
- print('Please enter y, n, a, or q')
107
- if quit:
95
+ if response is None or response == 'quit':
96
+ log.info('Aborting')
108
97
  break
98
+ elif response == 'no':
99
+ log.info('Skipped')
100
+ continue
101
+
102
+ if delete_changelist_alias(name, workspace_dir):
103
+ log.success('Deleted')
109
104
 
110
- log.success(f'Deleted {deleted_count} alias(es)')
111
105
  return 0
112
106
 
113
107
 
@@ -115,8 +109,8 @@ def alias_command(args: argparse.Namespace) -> int:
115
109
  """Dispatch alias subcommands."""
116
110
  if args.alias_action == 'list':
117
111
  return alias_list_command(args)
118
- elif args.alias_action == 'set':
119
- return alias_set_command(args)
112
+ elif args.alias_action == 'new':
113
+ return alias_new_command(args)
120
114
  elif args.alias_action == 'delete':
121
115
  return alias_delete_command(args)
122
116
  elif args.alias_action == 'clean':
@@ -227,25 +227,25 @@ Examples:
227
227
  description='List all changelist aliases stored in .git-p4son/changelists/'
228
228
  )
229
229
 
230
- # alias set
231
- alias_set_parser = alias_subparsers.add_parser(
232
- 'set',
230
+ # alias new
231
+ alias_new_parser = alias_subparsers.add_parser(
232
+ 'new',
233
233
  help='Save a changelist number under a named alias',
234
234
  description='Save a changelist number under a named alias in '
235
235
  '.git-p4son/changelists/<alias>'
236
236
  )
237
- alias_set_parser.add_argument(
237
+ alias_new_parser.add_argument(
238
238
  'changelist',
239
239
  help='Changelist number to save'
240
240
  )
241
- alias_set_parser.add_argument(
241
+ alias_new_parser.add_argument(
242
242
  'alias',
243
243
  nargs='?',
244
244
  default='branch',
245
245
  help='Alias name to save the changelist number under. '
246
246
  'Defaults to the current branch name'
247
247
  )
248
- alias_set_parser.add_argument(
248
+ alias_new_parser.add_argument(
249
249
  '-f', '--force',
250
250
  action='store_true',
251
251
  help='Overwrite an existing alias file'
@@ -396,7 +396,7 @@ def run_command(args: argparse.Namespace) -> int:
396
396
  elif args.command == 'update' and args.changelist == 'branch':
397
397
  branch_attr = 'changelist'
398
398
  elif (args.command == 'alias'
399
- and args.alias_action in ('set', 'delete')
399
+ and args.alias_action in ('new', 'delete')
400
400
  and args.alias == 'branch'):
401
401
  branch_attr = 'alias'
402
402
 
@@ -468,7 +468,7 @@ def main() -> int:
468
468
  seconds = int(args.sleep)
469
469
  log.heading(f'Sleeping for {seconds} seconds')
470
470
  time.sleep(seconds)
471
- log.ok('awake again')
471
+ log.success('awake again')
472
472
 
473
473
  return exit_code
474
474
  except KeyboardInterrupt:
@@ -128,7 +128,7 @@ def _complete_positional(command, subcommand, positional_count,
128
128
  branch_candidates = _get_branch_candidates(prefix, workspace_dir)
129
129
  return branch_candidates + _filter(aliases, prefix)
130
130
 
131
- if subcommand == 'set' and positional_count == 1:
131
+ if subcommand == 'new' and positional_count == 1:
132
132
  branch_candidates = _get_branch_candidates(prefix, workspace_dir)
133
133
  return branch_candidates + _filter(aliases, prefix)
134
134
 
@@ -177,26 +177,46 @@ 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
+ run(['p4', 'revert', filename], cwd=workspace_dir, dry_run=dry_run)
211
+ # For add -> delete: the file never existed in the depot, so just revert.
212
+ if current_action == 'add' and p4_action == 'delete':
213
+ return
197
214
  run(['p4', p4_action, '-c', changelist, filename],
198
215
  cwd=workspace_dir, dry_run=dry_run)
199
- elif current != changelist:
216
+ if p4_action != 'delete':
217
+ run(['git', 'restore', filename],
218
+ cwd=workspace_dir, dry_run=dry_run)
219
+ elif current_cl != changelist:
200
220
  run(['p4', 'reopen', '-c', changelist, filename],
201
221
  cwd=workspace_dir, dry_run=dry_run)
202
222
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-p4son
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -418,12 +418,12 @@ git p4son alias list
418
418
  git p4son alias list
419
419
  ```
420
420
 
421
- #### alias set
421
+ #### alias new
422
422
 
423
423
  Save a changelist number under a named alias:
424
424
 
425
425
  ```sh
426
- git p4son alias set <changelist> [alias] [--force]
426
+ git p4son alias new <changelist> [alias] [--force]
427
427
  ```
428
428
 
429
429
  **Arguments:**
@@ -435,9 +435,9 @@ git p4son alias set <changelist> [alias] [--force]
435
435
 
436
436
  **Examples:**
437
437
  ```sh
438
- git p4son alias set 12345 # alias defaults to branch name
439
- git p4son alias set 12345 myfeature
440
- git p4son alias set 67890 myfeature -f
438
+ git p4son alias new 12345 # alias defaults to branch name
439
+ git p4son alias new 12345 myfeature
440
+ git p4son alias new 67890 myfeature -f
441
441
  ```
442
442
 
443
443
  #### alias delete
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-p4son"
7
- version = "0.2.5"
7
+ version = "0.2.7"
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"
@@ -198,7 +198,7 @@ class TestComplete(unittest.TestCase):
198
198
  result = _complete(self.parser, ['alias', ''], workspace_dir='/ws')
199
199
  names = self._names(result)
200
200
  self.assertIn('list', names)
201
- self.assertIn('set', names)
201
+ self.assertIn('new', names)
202
202
  self.assertIn('delete', names)
203
203
  self.assertIn('clean', names)
204
204
 
@@ -208,14 +208,14 @@ class TestComplete(unittest.TestCase):
208
208
  names = self._names(result)
209
209
  self.assertEqual(names, ['myalias'])
210
210
 
211
- def test_alias_set_second_positional(self, _ws, _aliases):
212
- result = _complete(self.parser, ['alias', 'set', '123', ''],
211
+ def test_alias_new_second_positional(self, _ws, _aliases):
212
+ result = _complete(self.parser, ['alias', 'new', '123', ''],
213
213
  workspace_dir='/ws')
214
214
  names = self._names(result)
215
215
  self.assertEqual(names, ['myalias'])
216
216
 
217
- def test_alias_set_flags(self, _ws, _aliases):
218
- result = _complete(self.parser, ['alias', 'set', '-'],
217
+ def test_alias_new_flags(self, _ws, _aliases):
218
+ result = _complete(self.parser, ['alias', 'new', '-'],
219
219
  workspace_dir='/ws')
220
220
  names = self._names(result)
221
221
  self.assertIn('-f', names)
@@ -283,20 +283,20 @@ class TestCompleteBranchAlias(unittest.TestCase):
283
283
  self.assertIn('branch', names)
284
284
  self.assertIn('myalias', names)
285
285
 
286
- def test_alias_set_branch_keyword(self, _ws, _aliases, _branch):
287
- result = _complete(self.parser, ['alias', 'set', '123', 'b'],
286
+ def test_alias_new_branch_keyword(self, _ws, _aliases, _branch):
287
+ result = _complete(self.parser, ['alias', 'new', '123', 'b'],
288
288
  workspace_dir='/ws')
289
289
  names = self._names(result)
290
290
  self.assertIn('branch', names)
291
291
 
292
- def test_alias_set_br_prefix(self, _ws, _aliases, _branch):
293
- result = _complete(self.parser, ['alias', 'set', '123', 'br'],
292
+ def test_alias_new_br_prefix(self, _ws, _aliases, _branch):
293
+ result = _complete(self.parser, ['alias', 'new', '123', 'br'],
294
294
  workspace_dir='/ws')
295
295
  names = self._names(result)
296
296
  self.assertIn('branch', names)
297
297
 
298
- def test_alias_set_branch_expand(self, _ws, _aliases, _branch):
299
- result = _complete(self.parser, ['alias', 'set', '123', 'branch'],
298
+ def test_alias_new_branch_expand(self, _ws, _aliases, _branch):
299
+ result = _complete(self.parser, ['alias', 'new', '123', 'branch'],
300
300
  workspace_dir='/ws')
301
301
  names = self._names(result)
302
302
  self.assertIn('feat-cool', names)
@@ -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,80 @@ 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 -> delete (revert only, no reopen) ---
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_delete_reverts_only(self, mock_run, mock_check):
357
+ mock_run.return_value = make_run_result()
358
+ changes = LocalChanges()
359
+ changes.dels = ['file.txt']
360
+ include_changes_in_changelist(changes, '100', '/ws')
361
+ calls = mock_run.call_args_list
362
+ self.assertEqual(len(calls), 1)
363
+ self.assertEqual(calls[0][0][0], ['p4', 'revert', 'file.txt'])
364
+
365
+
291
366
  class TestOpenChangesForEdit(unittest.TestCase):
292
367
  @mock.patch('git_p4son.lib.include_changes_in_changelist')
293
368
  @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