Last active
June 30, 2021 18:29
-
-
Save bohdon/ba9899728cc2fc7fe7b60680c6a69f74 to your computer and use it in GitHub Desktop.
Find files in P4 that differ by name only in case
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #! 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