Skip to content

Instantly share code, notes, and snippets.

@bohdon
Last active June 30, 2021 18:29
Show Gist options
  • Select an option

  • Save bohdon/ba9899728cc2fc7fe7b60680c6a69f74 to your computer and use it in GitHub Desktop.

Select an option

Save bohdon/ba9899728cc2fc7fe7b60680c6a69f74 to your computer and use it in GitHub Desktop.
Find files in P4 that differ by name only in case
#! python
"""
Util for finding files in perforce that differ only in case.
Usage: p4_find_caseonly_files.py [OPTIONS] ROOT_PATH
Options:
--deleted
--output-obliterate TEXT File to write with list of deleted conflicting
files that can be obliterated
--help Show this message and exit.
Example usage with obliterate:
WARNING: ALWAYS verify the full list of files being obliterated before running with -y.
1 - run the util to find case conflicts, and output deleted conflicting files to a text file for obliterating
$ p4_find_caseonly_files.py --deleted --output-obliterate obliterate_files.txt //MyDepot/...
2.1 - look for any messages in the p4_find_caseonly_files.py output that says: "all conflicting paths were deleted,
electing to permanently delete" and individually verify that the desired files to obliterate are listed in obliterate_files.txt
ex log output:
//MyDepot/myFile.txt (headAction: delete, headTime: 2016-11-29 22:36:35)
//MyDepot/Myfile.txt (headAction: delete, headTime: 2016-11-29 22:36:35)
all conflicting paths were deleted, electing to permanently delete:
//MyDepot/Myfile.txt
ex obliterate_files.txt
//MyDepot/Myfile.txt
2.2 - if the elected file to be obliterated is not what you want, copy paste in the desired files to obliterate into obliterate_files.txt
2.3 - if the output from p4_find_caseonly_files.py is too long, you can output to a log file for easier review
$ p4_find_caseonly_files.py --deleted --output-obliterate obliterate_files.txt //MyDepot/... > case_conflicts.log
3.1 - After verifying the contents of obliterate_files.txt, dry-run the obliterate
$ p4 -x obliterate_files.txt obliterate
...
3.2 - Verify that the results from the dry-run look correct (triple check!), then run with the -y flag
$ p4 -x obliterate_files.txt obliterate -y
...
"""
from datetime import datetime
from operator import itemgetter
from os import fstat, path
import click
import P4
@click.command()
@click.argument('root_path')
@click.option('--deleted', is_flag=True)
@click.option('--output-obliterate', help="File to write with list of deleted conflicting files that can be obliterated")
def find(root_path, deleted=False, output_obliterate=None):
print('Connecting to p4...')
p4 = P4.P4()
p4.connect()
print(f'Connection established: {p4}')
args = []
if not deleted:
args.append('-e')
args.append(root_path)
print(f'Getting all files matching search pattern: {root_path}')
files = p4.run_files(*args)
deleted_conflicts = find_caseonly_files(files, p4)
if output_obliterate and deleted_conflicts:
with open(output_obliterate, 'w') as fp:
fp.write('\n'.join(deleted_conflicts))
def find_caseonly_files(files, p4):
print(f'Searching {len(files)} files for conflicts...')
# keep track of deleted conflicting files
deleted_conflicts = []
# map of all files with the same case insensitive path
files_by_path = {}
files_to_fstat = []
for file in files:
file_path = file['depotFile']
file_path_lower = file_path.lower()
if file_path_lower not in files_by_path:
files_by_path[file_path_lower] = []
files_by_path[file_path_lower].append(file_path)
files_to_fstat.append(file_path)
# filter for conflicts
conflicting_paths = [paths for _,
paths in files_by_path.items() if len(paths) > 1]
if not conflicting_paths:
print('')
print('No case conflicts found')
return
# get flast list of paths and run fstat on all of them
conflicting_paths_flat = [p for paths in conflicting_paths for p in paths]
print(f'Running fstat on {len(conflicting_paths_flat)} conflicting files...')
fstats = p4.run_fstat(conflicting_paths_flat)
# organize fstats by path for fast lookup
fstats_by_path = {f['depotFile']: f for f in fstats}
# print results, including some fstat info, and find deleted conflicting files
for paths in conflicting_paths:
these_deleted_conflicts = []
is_all_deleted = True
for path in paths:
fstat = fstats_by_path.get(path)
if not fstat:
print(f"Warning: fstat not found for path: {path}")
continue
head_time = fstat.get('headTime')
if head_time:
head_time_readable = datetime.utcfromtimestamp(int(head_time))
action = fstat.get('headAction')
msg = f'{path} (headAction: {action}, headTime: {head_time_readable})'
print(msg)
if action in ('delete', 'move/delete', 'purge'):
these_deleted_conflicts.append((path, head_time))
else:
is_all_deleted = False
if is_all_deleted:
# sort by times, then keep the latest deleted revision
these_deleted_conflicts.sort(key=itemgetter(1))
kept_path = these_deleted_conflicts.pop(-1)
print('')
print(
' all conflicting paths were deleted, electing to permanently delete:')
for path, _ in these_deleted_conflicts:
print(f' {path}')
deleted_conflicts.extend([p for p, _ in these_deleted_conflicts])
print('')
print('Deleted conflicting files:')
print(' (note that if all files of a conflict are deleted, the most recent will not be listed here)')
for path in deleted_conflicts:
print(f' {path}')
print('')
print(
f'Found {len(conflicting_paths)} case conflicts, {len(deleted_conflicts)} elected to obliterate')
return deleted_conflicts
if __name__ == '__main__':
find()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment