pyjallib.perforce
P4Python을 사용하는 Perforce 모듈.
이 모듈은 P4Python을 사용하여 Perforce 서버와 상호작용하는 기능을 제공합니다. 주요 기능:
- 워크스페이스 연결
- 체인지리스트 관리 (생성, 조회, 편집, 제출, 되돌리기)
- 파일 작업 (체크아웃, 추가, 삭제)
- 파일 동기화 및 업데이트 확인
1""" 2P4Python을 사용하는 Perforce 모듈. 3 4이 모듈은 P4Python을 사용하여 Perforce 서버와 상호작용하는 기능을 제공합니다. 5주요 기능: 6- 워크스페이스 연결 7- 체인지리스트 관리 (생성, 조회, 편집, 제출, 되돌리기) 8- 파일 작업 (체크아웃, 추가, 삭제) 9- 파일 동기화 및 업데이트 확인 10""" 11 12import logging 13from P4 import P4, P4Exception 14import os 15from pathlib import Path 16 17# 로깅 설정 18logger = logging.getLogger(__name__) 19logger.setLevel(logging.DEBUG) 20# 사용자 문서 폴더 내 로그 파일 저장 21log_path = os.path.join(Path.home() / "Documents", 'Perforce.log') 22file_handler = logging.FileHandler(log_path, encoding='utf-8') 23file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) 24logger.addHandler(file_handler) 25 26 27class Perforce: 28 """P4Python을 사용하여 Perforce 작업을 수행하는 클래스.""" 29 30 def __init__(self): 31 """Perforce 인스턴스를 초기화합니다.""" 32 self.p4 = P4() 33 self.connected = False 34 self.workspaceRoot = r"" 35 logger.info("Perforce 인스턴스 생성됨") 36 37 def _is_connected(self) -> bool: 38 """Perforce 서버 연결 상태를 확인합니다. 39 40 Returns: 41 bool: 연결되어 있으면 True, 아니면 False 42 """ 43 if not self.connected: 44 logger.warning("Perforce 서버에 연결되지 않았습니다.") 45 return False 46 return True 47 48 def _handle_p4_exception(self, e: P4Exception, context_msg: str = "") -> None: 49 """P4Exception을 처리하고 로깅합니다. 50 51 Args: 52 e (P4Exception): 발생한 예외 53 context_msg (str, optional): 예외가 발생한 컨텍스트 설명 54 """ 55 logger.error(f"{context_msg} 중 P4Exception 발생: {e}") 56 for err in self.p4.errors: 57 logger.error(f" P4 Error: {err}") 58 for warn in self.p4.warnings: 59 logger.warning(f" P4 Warning: {warn}") 60 61 def connect(self, workspace_name: str) -> bool: 62 """지정된 워크스페이스에 연결합니다. 63 64 Args: 65 workspace_name (str): 연결할 워크스페이스 이름 66 67 Returns: 68 bool: 연결 성공 시 True, 실패 시 False 69 """ 70 logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...") 71 try: 72 self.p4.client = workspace_name 73 self.p4.connect() 74 self.connected = True 75 76 # 워크스페이스 루트 경로 가져오기 77 try: 78 client_info = self.p4.run_client("-o", workspace_name)[0] 79 root_path = client_info.get("Root", "") 80 81 # Windows 경로 형식으로 변환 (슬래시를 백슬래시로) 82 root_path = os.path.normpath(root_path) 83 84 self.workspaceRoot = root_path 85 logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}") 86 except (IndexError, KeyError) as e: 87 logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}") 88 self.workspaceRoot = "" 89 90 logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})") 91 return True 92 except P4Exception as e: 93 self.connected = False 94 self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결") 95 return False 96 97 def get_pending_change_list(self) -> list: 98 """워크스페이스의 Pending된 체인지 리스트를 가져옵니다. 99 100 Returns: 101 list: 체인지 리스트 정보 딕셔너리들의 리스트 102 """ 103 if not self._is_connected(): 104 return [] 105 logger.debug("Pending 체인지 리스트 조회 중...") 106 try: 107 pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 108 change_numbers = [int(cl['change']) for cl in pending_changes] 109 110 # 각 체인지 리스트 번호에 대한 상세 정보 가져오기 111 change_list_info = [] 112 for change_number in change_numbers: 113 cl_info = self.get_change_list_by_number(change_number) 114 if cl_info: 115 change_list_info.append(cl_info) 116 117 logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료") 118 return change_list_info 119 except P4Exception as e: 120 self._handle_p4_exception(e, "Pending 체인지 리스트 조회") 121 return [] 122 123 def create_change_list(self, description: str) -> dict: 124 """새로운 체인지 리스트를 생성합니다. 125 126 Args: 127 description (str): 체인지 리스트 설명 128 129 Returns: 130 dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리 131 """ 132 if not self._is_connected(): 133 return {} 134 logger.info(f"새 체인지 리스트 생성 시도: '{description}'") 135 try: 136 change_spec = self.p4.fetch_change() 137 change_spec["Description"] = description 138 result = self.p4.save_change(change_spec) 139 created_change_number = int(result[0].split()[1]) 140 logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'") 141 return self.get_change_list_by_number(created_change_number) 142 except P4Exception as e: 143 self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')") 144 return {} 145 except (IndexError, ValueError) as e: 146 logger.error(f"체인지 리스트 번호 파싱 오류: {e}") 147 return {} 148 149 def get_change_list_by_number(self, change_list_number: int) -> dict: 150 """체인지 리스트 번호로 체인지 리스트를 가져옵니다. 151 152 Args: 153 change_list_number (int): 체인지 리스트 번호 154 155 Returns: 156 dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리 157 """ 158 if not self._is_connected(): 159 return {} 160 logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...") 161 try: 162 cl_info = self.p4.fetch_change(change_list_number) 163 if cl_info: 164 logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.") 165 return cl_info 166 else: 167 logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.") 168 return {} 169 except P4Exception as e: 170 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회") 171 return {} 172 173 def get_change_list_by_description(self, description: str) -> dict: 174 """체인지 리스트 설명으로 체인지 리스트를 가져옵니다. 175 176 Args: 177 description (str): 체인지 리스트 설명 178 179 Returns: 180 dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트) 181 """ 182 if not self._is_connected(): 183 return {} 184 logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'") 185 try: 186 pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 187 for cl in pending_changes: 188 cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip() 189 if cl_desc == description.strip(): 190 logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.") 191 return self.get_change_list_by_number(int(cl['change'])) 192 logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.") 193 return {} 194 except P4Exception as e: 195 self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')") 196 return {} 197 198 def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict: 199 """체인지 리스트를 편집합니다. 200 201 Args: 202 change_list_number (int): 체인지 리스트 번호 203 description (str, optional): 변경할 설명 204 add_file_paths (list, optional): 추가할 파일 경로 리스트 205 remove_file_paths (list, optional): 제거할 파일 경로 리스트 206 207 Returns: 208 dict: 업데이트된 체인지 리스트 정보 209 """ 210 if not self._is_connected(): 211 return {} 212 logger.info(f"체인지 리스트 {change_list_number} 편집 시도...") 213 try: 214 if description is not None: 215 change_spec = self.p4.fetch_change(change_list_number) 216 current_description = change_spec.get('Description', '').strip() 217 if current_description != description.strip(): 218 change_spec['Description'] = description 219 self.p4.save_change(change_spec) 220 logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'") 221 222 if add_file_paths: 223 for file_path in add_file_paths: 224 try: 225 self.p4.run_reopen("-c", change_list_number, file_path) 226 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.") 227 except P4Exception as e_reopen: 228 self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동") 229 230 if remove_file_paths: 231 for file_path in remove_file_paths: 232 try: 233 self.p4.run_revert("-c", change_list_number, file_path) 234 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.") 235 except P4Exception as e_revert: 236 self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)") 237 238 return self.get_change_list_by_number(change_list_number) 239 240 except P4Exception as e: 241 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 편집") 242 return self.get_change_list_by_number(change_list_number) 243 244 def _file_op(self, command: str, file_path: str, change_list_number: int, op_name: str) -> bool: 245 """파일 작업을 수행하는 내부 헬퍼 함수입니다. 246 247 Args: 248 command (str): 실행할 명령어 (edit/add/delete) 249 file_path (str): 대상 파일 경로 250 change_list_number (int): 체인지 리스트 번호 251 op_name (str): 작업 이름 (로깅용) 252 253 Returns: 254 bool: 작업 성공 시 True, 실패 시 False 255 """ 256 if not self._is_connected(): 257 return False 258 logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 시도 (CL: {change_list_number})...") 259 try: 260 if command == "edit": 261 self.p4.run_edit("-c", change_list_number, file_path) 262 elif command == "add": 263 self.p4.run_add("-c", change_list_number, file_path) 264 elif command == "delete": 265 self.p4.run_delete("-c", change_list_number, file_path) 266 else: 267 logger.error(f"지원되지 않는 파일 작업: {command}") 268 return False 269 logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 성공 (CL: {change_list_number}).") 270 return True 271 except P4Exception as e: 272 self._handle_p4_exception(e, f"파일 '{file_path}' {op_name} (CL: {change_list_number})") 273 return False 274 275 def checkout_file(self, file_path: str, change_list_number: int) -> bool: 276 """파일을 체크아웃합니다. 277 278 Args: 279 file_path (str): 체크아웃할 파일 경로 280 change_list_number (int): 체인지 리스트 번호 281 282 Returns: 283 bool: 체크아웃 성공 시 True, 실패 시 False 284 """ 285 return self._file_op("edit", file_path, change_list_number, "체크아웃") 286 287 def add_file(self, file_path: str, change_list_number: int) -> bool: 288 """파일을 추가합니다. 289 290 Args: 291 file_path (str): 추가할 파일 경로 292 change_list_number (int): 체인지 리스트 번호 293 294 Returns: 295 bool: 추가 성공 시 True, 실패 시 False 296 """ 297 return self._file_op("add", file_path, change_list_number, "추가") 298 299 def delete_file(self, file_path: str, change_list_number: int) -> bool: 300 """파일을 삭제합니다. 301 302 Args: 303 file_path (str): 삭제할 파일 경로 304 change_list_number (int): 체인지 리스트 번호 305 306 Returns: 307 bool: 삭제 성공 시 True, 실패 시 False 308 """ 309 return self._file_op("delete", file_path, change_list_number, "삭제") 310 311 def submit_change_list(self, change_list_number: int) -> bool: 312 """체인지 리스트를 제출합니다. 313 314 Args: 315 change_list_number (int): 제출할 체인지 리스트 번호 316 317 Returns: 318 bool: 제출 성공 시 True, 실패 시 False 319 """ 320 if not self._is_connected(): 321 return False 322 logger.info(f"체인지 리스트 {change_list_number} 제출 시도...") 323 try: 324 self.p4.run_submit("-c", change_list_number) 325 logger.info(f"체인지 리스트 {change_list_number} 제출 성공.") 326 return True 327 except P4Exception as e: 328 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출") 329 if any("nothing to submit" in err.lower() for err in self.p4.errors): 330 logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.") 331 return False 332 333 def revert_change_list(self, change_list_number: int) -> bool: 334 """체인지 리스트를 되돌리고 삭제합니다. 335 336 체인지 리스트 내 모든 파일을 되돌린 후 빈 체인지 리스트를 삭제합니다. 337 338 Args: 339 change_list_number (int): 되돌릴 체인지 리스트 번호 340 341 Returns: 342 bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False 343 """ 344 if not self._is_connected(): 345 return False 346 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...") 347 try: 348 # 체인지 리스트의 모든 파일 되돌리기 349 self.p4.run_revert("-c", change_list_number, "//...") 350 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.") 351 352 # 빈 체인지 리스트 삭제 353 try: 354 self.p4.run_change("-d", change_list_number) 355 logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.") 356 except P4Exception as e_delete: 357 self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제") 358 logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.") 359 return False 360 361 return True 362 except P4Exception as e: 363 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기") 364 return False 365 366 def delete_empty_change_list(self, change_list_number: int) -> bool: 367 """빈 체인지 리스트를 삭제합니다. 368 369 Args: 370 change_list_number (int): 삭제할 체인지 리스트 번호 371 372 Returns: 373 bool: 삭제 성공 시 True, 실패 시 False 374 """ 375 if not self._is_connected(): 376 return False 377 378 logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...") 379 try: 380 # 체인지 리스트 정보 가져오기 381 change_spec = self.p4.fetch_change(change_list_number) 382 383 # 파일이 있는지 확인 384 if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0: 385 logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.") 386 return False 387 388 # 빈 체인지 리스트 삭제 389 self.p4.run_change("-d", change_list_number) 390 logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.") 391 return True 392 except P4Exception as e: 393 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제") 394 return False 395 396 def revert_files(self, change_list_number: int, file_paths: list) -> bool: 397 """체인지 리스트 내의 특정 파일들을 되돌립니다. 398 399 Args: 400 change_list_number (int): 체인지 리스트 번호 401 file_paths (list): 되돌릴 파일 경로 리스트 402 403 Returns: 404 bool: 되돌리기 성공 시 True, 실패 시 False 405 """ 406 if not self._is_connected(): 407 return False 408 if not file_paths: 409 logger.warning("되돌릴 파일 목록이 비어있습니다.") 410 return True 411 412 logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...") 413 try: 414 for file_path in file_paths: 415 self.p4.run_revert("-c", change_list_number, file_path) 416 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.") 417 return True 418 except P4Exception as e: 419 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기") 420 return False 421 422 def check_update_required(self, file_paths: list) -> bool: 423 """파일이나 폴더의 업데이트 필요 여부를 확인합니다. 424 425 Args: 426 file_paths (list): 확인할 파일 또는 폴더 경로 리스트. 427 폴더 경로는 자동으로 재귀적으로 처리됩니다. 428 429 Returns: 430 bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False 431 """ 432 if not self._is_connected(): 433 return False 434 if not file_paths: 435 logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.") 436 return False 437 438 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 439 processed_paths = [] 440 for path in file_paths: 441 if os.path.isdir(path): 442 # 폴더 경로에 '...'(재귀) 패턴을 추가 443 processed_paths.append(os.path.join(path, '...')) 444 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 445 else: 446 processed_paths.append(path) 447 448 logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}") 449 try: 450 sync_preview_results = self.p4.run_sync("-n", processed_paths) 451 needs_update = False 452 for result in sync_preview_results: 453 if isinstance(result, dict): 454 if 'up-to-date' not in result.get('how', '') and \ 455 'no such file(s)' not in result.get('depotFile', ''): 456 if result.get('how') and 'syncing' in result.get('how'): 457 needs_update = True 458 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}") 459 break 460 elif result.get('action') and result.get('action') not in ['checked', 'exists']: 461 needs_update = True 462 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})") 463 break 464 elif isinstance(result, str): 465 if "up-to-date" not in result and "no such file(s)" not in result: 466 needs_update = True 467 logger.info(f"파일 업데이트 필요 (문자열 결과): {result}") 468 break 469 470 if needs_update: 471 logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.") 472 else: 473 logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.") 474 return needs_update 475 except P4Exception as e: 476 self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})") 477 return False 478 479 def sync_files(self, file_paths: list) -> bool: 480 """파일이나 폴더를 동기화합니다. 481 482 Args: 483 file_paths (list): 동기화할 파일 또는 폴더 경로 리스트. 484 폴더 경로는 자동으로 재귀적으로 처리됩니다. 485 486 Returns: 487 bool: 동기화 성공 시 True, 실패 시 False 488 """ 489 if not self._is_connected(): 490 return False 491 if not file_paths: 492 logger.debug("싱크할 파일/폴더 목록이 비어있습니다.") 493 return True 494 495 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 496 processed_paths = [] 497 for path in file_paths: 498 if os.path.isdir(path): 499 # 폴더 경로에 '...'(재귀) 패턴을 추가 500 processed_paths.append(os.path.join(path, '...')) 501 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 502 else: 503 processed_paths.append(path) 504 505 logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}") 506 try: 507 self.p4.run_sync(processed_paths) 508 logger.info(f"파일/폴더 싱크 완료: {processed_paths}") 509 return True 510 except P4Exception as e: 511 self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})") 512 return False 513 514 def disconnect(self): 515 """Perforce 서버와의 연결을 해제합니다.""" 516 if self.connected: 517 try: 518 self.p4.disconnect() 519 self.connected = False 520 logger.info("Perforce 서버 연결 해제 완료.") 521 except P4Exception as e: 522 self._handle_p4_exception(e, "Perforce 서버 연결 해제") 523 else: 524 logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.") 525 526 def __del__(self): 527 """객체가 소멸될 때 자동으로 연결을 해제합니다.""" 528 self.disconnect()
28class Perforce: 29 """P4Python을 사용하여 Perforce 작업을 수행하는 클래스.""" 30 31 def __init__(self): 32 """Perforce 인스턴스를 초기화합니다.""" 33 self.p4 = P4() 34 self.connected = False 35 self.workspaceRoot = r"" 36 logger.info("Perforce 인스턴스 생성됨") 37 38 def _is_connected(self) -> bool: 39 """Perforce 서버 연결 상태를 확인합니다. 40 41 Returns: 42 bool: 연결되어 있으면 True, 아니면 False 43 """ 44 if not self.connected: 45 logger.warning("Perforce 서버에 연결되지 않았습니다.") 46 return False 47 return True 48 49 def _handle_p4_exception(self, e: P4Exception, context_msg: str = "") -> None: 50 """P4Exception을 처리하고 로깅합니다. 51 52 Args: 53 e (P4Exception): 발생한 예외 54 context_msg (str, optional): 예외가 발생한 컨텍스트 설명 55 """ 56 logger.error(f"{context_msg} 중 P4Exception 발생: {e}") 57 for err in self.p4.errors: 58 logger.error(f" P4 Error: {err}") 59 for warn in self.p4.warnings: 60 logger.warning(f" P4 Warning: {warn}") 61 62 def connect(self, workspace_name: str) -> bool: 63 """지정된 워크스페이스에 연결합니다. 64 65 Args: 66 workspace_name (str): 연결할 워크스페이스 이름 67 68 Returns: 69 bool: 연결 성공 시 True, 실패 시 False 70 """ 71 logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...") 72 try: 73 self.p4.client = workspace_name 74 self.p4.connect() 75 self.connected = True 76 77 # 워크스페이스 루트 경로 가져오기 78 try: 79 client_info = self.p4.run_client("-o", workspace_name)[0] 80 root_path = client_info.get("Root", "") 81 82 # Windows 경로 형식으로 변환 (슬래시를 백슬래시로) 83 root_path = os.path.normpath(root_path) 84 85 self.workspaceRoot = root_path 86 logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}") 87 except (IndexError, KeyError) as e: 88 logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}") 89 self.workspaceRoot = "" 90 91 logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})") 92 return True 93 except P4Exception as e: 94 self.connected = False 95 self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결") 96 return False 97 98 def get_pending_change_list(self) -> list: 99 """워크스페이스의 Pending된 체인지 리스트를 가져옵니다. 100 101 Returns: 102 list: 체인지 리스트 정보 딕셔너리들의 리스트 103 """ 104 if not self._is_connected(): 105 return [] 106 logger.debug("Pending 체인지 리스트 조회 중...") 107 try: 108 pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 109 change_numbers = [int(cl['change']) for cl in pending_changes] 110 111 # 각 체인지 리스트 번호에 대한 상세 정보 가져오기 112 change_list_info = [] 113 for change_number in change_numbers: 114 cl_info = self.get_change_list_by_number(change_number) 115 if cl_info: 116 change_list_info.append(cl_info) 117 118 logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료") 119 return change_list_info 120 except P4Exception as e: 121 self._handle_p4_exception(e, "Pending 체인지 리스트 조회") 122 return [] 123 124 def create_change_list(self, description: str) -> dict: 125 """새로운 체인지 리스트를 생성합니다. 126 127 Args: 128 description (str): 체인지 리스트 설명 129 130 Returns: 131 dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리 132 """ 133 if not self._is_connected(): 134 return {} 135 logger.info(f"새 체인지 리스트 생성 시도: '{description}'") 136 try: 137 change_spec = self.p4.fetch_change() 138 change_spec["Description"] = description 139 result = self.p4.save_change(change_spec) 140 created_change_number = int(result[0].split()[1]) 141 logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'") 142 return self.get_change_list_by_number(created_change_number) 143 except P4Exception as e: 144 self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')") 145 return {} 146 except (IndexError, ValueError) as e: 147 logger.error(f"체인지 리스트 번호 파싱 오류: {e}") 148 return {} 149 150 def get_change_list_by_number(self, change_list_number: int) -> dict: 151 """체인지 리스트 번호로 체인지 리스트를 가져옵니다. 152 153 Args: 154 change_list_number (int): 체인지 리스트 번호 155 156 Returns: 157 dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리 158 """ 159 if not self._is_connected(): 160 return {} 161 logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...") 162 try: 163 cl_info = self.p4.fetch_change(change_list_number) 164 if cl_info: 165 logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.") 166 return cl_info 167 else: 168 logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.") 169 return {} 170 except P4Exception as e: 171 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회") 172 return {} 173 174 def get_change_list_by_description(self, description: str) -> dict: 175 """체인지 리스트 설명으로 체인지 리스트를 가져옵니다. 176 177 Args: 178 description (str): 체인지 리스트 설명 179 180 Returns: 181 dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트) 182 """ 183 if not self._is_connected(): 184 return {} 185 logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'") 186 try: 187 pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 188 for cl in pending_changes: 189 cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip() 190 if cl_desc == description.strip(): 191 logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.") 192 return self.get_change_list_by_number(int(cl['change'])) 193 logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.") 194 return {} 195 except P4Exception as e: 196 self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')") 197 return {} 198 199 def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict: 200 """체인지 리스트를 편집합니다. 201 202 Args: 203 change_list_number (int): 체인지 리스트 번호 204 description (str, optional): 변경할 설명 205 add_file_paths (list, optional): 추가할 파일 경로 리스트 206 remove_file_paths (list, optional): 제거할 파일 경로 리스트 207 208 Returns: 209 dict: 업데이트된 체인지 리스트 정보 210 """ 211 if not self._is_connected(): 212 return {} 213 logger.info(f"체인지 리스트 {change_list_number} 편집 시도...") 214 try: 215 if description is not None: 216 change_spec = self.p4.fetch_change(change_list_number) 217 current_description = change_spec.get('Description', '').strip() 218 if current_description != description.strip(): 219 change_spec['Description'] = description 220 self.p4.save_change(change_spec) 221 logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'") 222 223 if add_file_paths: 224 for file_path in add_file_paths: 225 try: 226 self.p4.run_reopen("-c", change_list_number, file_path) 227 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.") 228 except P4Exception as e_reopen: 229 self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동") 230 231 if remove_file_paths: 232 for file_path in remove_file_paths: 233 try: 234 self.p4.run_revert("-c", change_list_number, file_path) 235 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.") 236 except P4Exception as e_revert: 237 self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)") 238 239 return self.get_change_list_by_number(change_list_number) 240 241 except P4Exception as e: 242 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 편집") 243 return self.get_change_list_by_number(change_list_number) 244 245 def _file_op(self, command: str, file_path: str, change_list_number: int, op_name: str) -> bool: 246 """파일 작업을 수행하는 내부 헬퍼 함수입니다. 247 248 Args: 249 command (str): 실행할 명령어 (edit/add/delete) 250 file_path (str): 대상 파일 경로 251 change_list_number (int): 체인지 리스트 번호 252 op_name (str): 작업 이름 (로깅용) 253 254 Returns: 255 bool: 작업 성공 시 True, 실패 시 False 256 """ 257 if not self._is_connected(): 258 return False 259 logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 시도 (CL: {change_list_number})...") 260 try: 261 if command == "edit": 262 self.p4.run_edit("-c", change_list_number, file_path) 263 elif command == "add": 264 self.p4.run_add("-c", change_list_number, file_path) 265 elif command == "delete": 266 self.p4.run_delete("-c", change_list_number, file_path) 267 else: 268 logger.error(f"지원되지 않는 파일 작업: {command}") 269 return False 270 logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 성공 (CL: {change_list_number}).") 271 return True 272 except P4Exception as e: 273 self._handle_p4_exception(e, f"파일 '{file_path}' {op_name} (CL: {change_list_number})") 274 return False 275 276 def checkout_file(self, file_path: str, change_list_number: int) -> bool: 277 """파일을 체크아웃합니다. 278 279 Args: 280 file_path (str): 체크아웃할 파일 경로 281 change_list_number (int): 체인지 리스트 번호 282 283 Returns: 284 bool: 체크아웃 성공 시 True, 실패 시 False 285 """ 286 return self._file_op("edit", file_path, change_list_number, "체크아웃") 287 288 def add_file(self, file_path: str, change_list_number: int) -> bool: 289 """파일을 추가합니다. 290 291 Args: 292 file_path (str): 추가할 파일 경로 293 change_list_number (int): 체인지 리스트 번호 294 295 Returns: 296 bool: 추가 성공 시 True, 실패 시 False 297 """ 298 return self._file_op("add", file_path, change_list_number, "추가") 299 300 def delete_file(self, file_path: str, change_list_number: int) -> bool: 301 """파일을 삭제합니다. 302 303 Args: 304 file_path (str): 삭제할 파일 경로 305 change_list_number (int): 체인지 리스트 번호 306 307 Returns: 308 bool: 삭제 성공 시 True, 실패 시 False 309 """ 310 return self._file_op("delete", file_path, change_list_number, "삭제") 311 312 def submit_change_list(self, change_list_number: int) -> bool: 313 """체인지 리스트를 제출합니다. 314 315 Args: 316 change_list_number (int): 제출할 체인지 리스트 번호 317 318 Returns: 319 bool: 제출 성공 시 True, 실패 시 False 320 """ 321 if not self._is_connected(): 322 return False 323 logger.info(f"체인지 리스트 {change_list_number} 제출 시도...") 324 try: 325 self.p4.run_submit("-c", change_list_number) 326 logger.info(f"체인지 리스트 {change_list_number} 제출 성공.") 327 return True 328 except P4Exception as e: 329 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출") 330 if any("nothing to submit" in err.lower() for err in self.p4.errors): 331 logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.") 332 return False 333 334 def revert_change_list(self, change_list_number: int) -> bool: 335 """체인지 리스트를 되돌리고 삭제합니다. 336 337 체인지 리스트 내 모든 파일을 되돌린 후 빈 체인지 리스트를 삭제합니다. 338 339 Args: 340 change_list_number (int): 되돌릴 체인지 리스트 번호 341 342 Returns: 343 bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False 344 """ 345 if not self._is_connected(): 346 return False 347 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...") 348 try: 349 # 체인지 리스트의 모든 파일 되돌리기 350 self.p4.run_revert("-c", change_list_number, "//...") 351 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.") 352 353 # 빈 체인지 리스트 삭제 354 try: 355 self.p4.run_change("-d", change_list_number) 356 logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.") 357 except P4Exception as e_delete: 358 self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제") 359 logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.") 360 return False 361 362 return True 363 except P4Exception as e: 364 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기") 365 return False 366 367 def delete_empty_change_list(self, change_list_number: int) -> bool: 368 """빈 체인지 리스트를 삭제합니다. 369 370 Args: 371 change_list_number (int): 삭제할 체인지 리스트 번호 372 373 Returns: 374 bool: 삭제 성공 시 True, 실패 시 False 375 """ 376 if not self._is_connected(): 377 return False 378 379 logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...") 380 try: 381 # 체인지 리스트 정보 가져오기 382 change_spec = self.p4.fetch_change(change_list_number) 383 384 # 파일이 있는지 확인 385 if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0: 386 logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.") 387 return False 388 389 # 빈 체인지 리스트 삭제 390 self.p4.run_change("-d", change_list_number) 391 logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.") 392 return True 393 except P4Exception as e: 394 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제") 395 return False 396 397 def revert_files(self, change_list_number: int, file_paths: list) -> bool: 398 """체인지 리스트 내의 특정 파일들을 되돌립니다. 399 400 Args: 401 change_list_number (int): 체인지 리스트 번호 402 file_paths (list): 되돌릴 파일 경로 리스트 403 404 Returns: 405 bool: 되돌리기 성공 시 True, 실패 시 False 406 """ 407 if not self._is_connected(): 408 return False 409 if not file_paths: 410 logger.warning("되돌릴 파일 목록이 비어있습니다.") 411 return True 412 413 logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...") 414 try: 415 for file_path in file_paths: 416 self.p4.run_revert("-c", change_list_number, file_path) 417 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.") 418 return True 419 except P4Exception as e: 420 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기") 421 return False 422 423 def check_update_required(self, file_paths: list) -> bool: 424 """파일이나 폴더의 업데이트 필요 여부를 확인합니다. 425 426 Args: 427 file_paths (list): 확인할 파일 또는 폴더 경로 리스트. 428 폴더 경로는 자동으로 재귀적으로 처리됩니다. 429 430 Returns: 431 bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False 432 """ 433 if not self._is_connected(): 434 return False 435 if not file_paths: 436 logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.") 437 return False 438 439 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 440 processed_paths = [] 441 for path in file_paths: 442 if os.path.isdir(path): 443 # 폴더 경로에 '...'(재귀) 패턴을 추가 444 processed_paths.append(os.path.join(path, '...')) 445 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 446 else: 447 processed_paths.append(path) 448 449 logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}") 450 try: 451 sync_preview_results = self.p4.run_sync("-n", processed_paths) 452 needs_update = False 453 for result in sync_preview_results: 454 if isinstance(result, dict): 455 if 'up-to-date' not in result.get('how', '') and \ 456 'no such file(s)' not in result.get('depotFile', ''): 457 if result.get('how') and 'syncing' in result.get('how'): 458 needs_update = True 459 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}") 460 break 461 elif result.get('action') and result.get('action') not in ['checked', 'exists']: 462 needs_update = True 463 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})") 464 break 465 elif isinstance(result, str): 466 if "up-to-date" not in result and "no such file(s)" not in result: 467 needs_update = True 468 logger.info(f"파일 업데이트 필요 (문자열 결과): {result}") 469 break 470 471 if needs_update: 472 logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.") 473 else: 474 logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.") 475 return needs_update 476 except P4Exception as e: 477 self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})") 478 return False 479 480 def sync_files(self, file_paths: list) -> bool: 481 """파일이나 폴더를 동기화합니다. 482 483 Args: 484 file_paths (list): 동기화할 파일 또는 폴더 경로 리스트. 485 폴더 경로는 자동으로 재귀적으로 처리됩니다. 486 487 Returns: 488 bool: 동기화 성공 시 True, 실패 시 False 489 """ 490 if not self._is_connected(): 491 return False 492 if not file_paths: 493 logger.debug("싱크할 파일/폴더 목록이 비어있습니다.") 494 return True 495 496 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 497 processed_paths = [] 498 for path in file_paths: 499 if os.path.isdir(path): 500 # 폴더 경로에 '...'(재귀) 패턴을 추가 501 processed_paths.append(os.path.join(path, '...')) 502 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 503 else: 504 processed_paths.append(path) 505 506 logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}") 507 try: 508 self.p4.run_sync(processed_paths) 509 logger.info(f"파일/폴더 싱크 완료: {processed_paths}") 510 return True 511 except P4Exception as e: 512 self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})") 513 return False 514 515 def disconnect(self): 516 """Perforce 서버와의 연결을 해제합니다.""" 517 if self.connected: 518 try: 519 self.p4.disconnect() 520 self.connected = False 521 logger.info("Perforce 서버 연결 해제 완료.") 522 except P4Exception as e: 523 self._handle_p4_exception(e, "Perforce 서버 연결 해제") 524 else: 525 logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.") 526 527 def __del__(self): 528 """객체가 소멸될 때 자동으로 연결을 해제합니다.""" 529 self.disconnect()
P4Python을 사용하여 Perforce 작업을 수행하는 클래스.
31 def __init__(self): 32 """Perforce 인스턴스를 초기화합니다.""" 33 self.p4 = P4() 34 self.connected = False 35 self.workspaceRoot = r"" 36 logger.info("Perforce 인스턴스 생성됨")
Perforce 인스턴스를 초기화합니다.
62 def connect(self, workspace_name: str) -> bool: 63 """지정된 워크스페이스에 연결합니다. 64 65 Args: 66 workspace_name (str): 연결할 워크스페이스 이름 67 68 Returns: 69 bool: 연결 성공 시 True, 실패 시 False 70 """ 71 logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...") 72 try: 73 self.p4.client = workspace_name 74 self.p4.connect() 75 self.connected = True 76 77 # 워크스페이스 루트 경로 가져오기 78 try: 79 client_info = self.p4.run_client("-o", workspace_name)[0] 80 root_path = client_info.get("Root", "") 81 82 # Windows 경로 형식으로 변환 (슬래시를 백슬래시로) 83 root_path = os.path.normpath(root_path) 84 85 self.workspaceRoot = root_path 86 logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}") 87 except (IndexError, KeyError) as e: 88 logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}") 89 self.workspaceRoot = "" 90 91 logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})") 92 return True 93 except P4Exception as e: 94 self.connected = False 95 self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결") 96 return False
지정된 워크스페이스에 연결합니다.
Args: workspace_name (str): 연결할 워크스페이스 이름
Returns: bool: 연결 성공 시 True, 실패 시 False
98 def get_pending_change_list(self) -> list: 99 """워크스페이스의 Pending된 체인지 리스트를 가져옵니다. 100 101 Returns: 102 list: 체인지 리스트 정보 딕셔너리들의 리스트 103 """ 104 if not self._is_connected(): 105 return [] 106 logger.debug("Pending 체인지 리스트 조회 중...") 107 try: 108 pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 109 change_numbers = [int(cl['change']) for cl in pending_changes] 110 111 # 각 체인지 리스트 번호에 대한 상세 정보 가져오기 112 change_list_info = [] 113 for change_number in change_numbers: 114 cl_info = self.get_change_list_by_number(change_number) 115 if cl_info: 116 change_list_info.append(cl_info) 117 118 logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료") 119 return change_list_info 120 except P4Exception as e: 121 self._handle_p4_exception(e, "Pending 체인지 리스트 조회") 122 return []
워크스페이스의 Pending된 체인지 리스트를 가져옵니다.
Returns: list: 체인지 리스트 정보 딕셔너리들의 리스트
124 def create_change_list(self, description: str) -> dict: 125 """새로운 체인지 리스트를 생성합니다. 126 127 Args: 128 description (str): 체인지 리스트 설명 129 130 Returns: 131 dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리 132 """ 133 if not self._is_connected(): 134 return {} 135 logger.info(f"새 체인지 리스트 생성 시도: '{description}'") 136 try: 137 change_spec = self.p4.fetch_change() 138 change_spec["Description"] = description 139 result = self.p4.save_change(change_spec) 140 created_change_number = int(result[0].split()[1]) 141 logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'") 142 return self.get_change_list_by_number(created_change_number) 143 except P4Exception as e: 144 self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')") 145 return {} 146 except (IndexError, ValueError) as e: 147 logger.error(f"체인지 리스트 번호 파싱 오류: {e}") 148 return {}
새로운 체인지 리스트를 생성합니다.
Args: description (str): 체인지 리스트 설명
Returns: dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리
150 def get_change_list_by_number(self, change_list_number: int) -> dict: 151 """체인지 리스트 번호로 체인지 리스트를 가져옵니다. 152 153 Args: 154 change_list_number (int): 체인지 리스트 번호 155 156 Returns: 157 dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리 158 """ 159 if not self._is_connected(): 160 return {} 161 logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...") 162 try: 163 cl_info = self.p4.fetch_change(change_list_number) 164 if cl_info: 165 logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.") 166 return cl_info 167 else: 168 logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.") 169 return {} 170 except P4Exception as e: 171 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회") 172 return {}
체인지 리스트 번호로 체인지 리스트를 가져옵니다.
Args: change_list_number (int): 체인지 리스트 번호
Returns: dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리
174 def get_change_list_by_description(self, description: str) -> dict: 175 """체인지 리스트 설명으로 체인지 리스트를 가져옵니다. 176 177 Args: 178 description (str): 체인지 리스트 설명 179 180 Returns: 181 dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트) 182 """ 183 if not self._is_connected(): 184 return {} 185 logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'") 186 try: 187 pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client) 188 for cl in pending_changes: 189 cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip() 190 if cl_desc == description.strip(): 191 logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.") 192 return self.get_change_list_by_number(int(cl['change'])) 193 logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.") 194 return {} 195 except P4Exception as e: 196 self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')") 197 return {}
체인지 리스트 설명으로 체인지 리스트를 가져옵니다.
Args: description (str): 체인지 리스트 설명
Returns: dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트)
199 def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict: 200 """체인지 리스트를 편집합니다. 201 202 Args: 203 change_list_number (int): 체인지 리스트 번호 204 description (str, optional): 변경할 설명 205 add_file_paths (list, optional): 추가할 파일 경로 리스트 206 remove_file_paths (list, optional): 제거할 파일 경로 리스트 207 208 Returns: 209 dict: 업데이트된 체인지 리스트 정보 210 """ 211 if not self._is_connected(): 212 return {} 213 logger.info(f"체인지 리스트 {change_list_number} 편집 시도...") 214 try: 215 if description is not None: 216 change_spec = self.p4.fetch_change(change_list_number) 217 current_description = change_spec.get('Description', '').strip() 218 if current_description != description.strip(): 219 change_spec['Description'] = description 220 self.p4.save_change(change_spec) 221 logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'") 222 223 if add_file_paths: 224 for file_path in add_file_paths: 225 try: 226 self.p4.run_reopen("-c", change_list_number, file_path) 227 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.") 228 except P4Exception as e_reopen: 229 self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동") 230 231 if remove_file_paths: 232 for file_path in remove_file_paths: 233 try: 234 self.p4.run_revert("-c", change_list_number, file_path) 235 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.") 236 except P4Exception as e_revert: 237 self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)") 238 239 return self.get_change_list_by_number(change_list_number) 240 241 except P4Exception as e: 242 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 편집") 243 return self.get_change_list_by_number(change_list_number)
체인지 리스트를 편집합니다.
Args: change_list_number (int): 체인지 리스트 번호 description (str, optional): 변경할 설명 add_file_paths (list, optional): 추가할 파일 경로 리스트 remove_file_paths (list, optional): 제거할 파일 경로 리스트
Returns: dict: 업데이트된 체인지 리스트 정보
276 def checkout_file(self, file_path: str, change_list_number: int) -> bool: 277 """파일을 체크아웃합니다. 278 279 Args: 280 file_path (str): 체크아웃할 파일 경로 281 change_list_number (int): 체인지 리스트 번호 282 283 Returns: 284 bool: 체크아웃 성공 시 True, 실패 시 False 285 """ 286 return self._file_op("edit", file_path, change_list_number, "체크아웃")
파일을 체크아웃합니다.
Args: file_path (str): 체크아웃할 파일 경로 change_list_number (int): 체인지 리스트 번호
Returns: bool: 체크아웃 성공 시 True, 실패 시 False
288 def add_file(self, file_path: str, change_list_number: int) -> bool: 289 """파일을 추가합니다. 290 291 Args: 292 file_path (str): 추가할 파일 경로 293 change_list_number (int): 체인지 리스트 번호 294 295 Returns: 296 bool: 추가 성공 시 True, 실패 시 False 297 """ 298 return self._file_op("add", file_path, change_list_number, "추가")
파일을 추가합니다.
Args: file_path (str): 추가할 파일 경로 change_list_number (int): 체인지 리스트 번호
Returns: bool: 추가 성공 시 True, 실패 시 False
300 def delete_file(self, file_path: str, change_list_number: int) -> bool: 301 """파일을 삭제합니다. 302 303 Args: 304 file_path (str): 삭제할 파일 경로 305 change_list_number (int): 체인지 리스트 번호 306 307 Returns: 308 bool: 삭제 성공 시 True, 실패 시 False 309 """ 310 return self._file_op("delete", file_path, change_list_number, "삭제")
파일을 삭제합니다.
Args: file_path (str): 삭제할 파일 경로 change_list_number (int): 체인지 리스트 번호
Returns: bool: 삭제 성공 시 True, 실패 시 False
312 def submit_change_list(self, change_list_number: int) -> bool: 313 """체인지 리스트를 제출합니다. 314 315 Args: 316 change_list_number (int): 제출할 체인지 리스트 번호 317 318 Returns: 319 bool: 제출 성공 시 True, 실패 시 False 320 """ 321 if not self._is_connected(): 322 return False 323 logger.info(f"체인지 리스트 {change_list_number} 제출 시도...") 324 try: 325 self.p4.run_submit("-c", change_list_number) 326 logger.info(f"체인지 리스트 {change_list_number} 제출 성공.") 327 return True 328 except P4Exception as e: 329 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출") 330 if any("nothing to submit" in err.lower() for err in self.p4.errors): 331 logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.") 332 return False
체인지 리스트를 제출합니다.
Args: change_list_number (int): 제출할 체인지 리스트 번호
Returns: bool: 제출 성공 시 True, 실패 시 False
334 def revert_change_list(self, change_list_number: int) -> bool: 335 """체인지 리스트를 되돌리고 삭제합니다. 336 337 체인지 리스트 내 모든 파일을 되돌린 후 빈 체인지 리스트를 삭제합니다. 338 339 Args: 340 change_list_number (int): 되돌릴 체인지 리스트 번호 341 342 Returns: 343 bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False 344 """ 345 if not self._is_connected(): 346 return False 347 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...") 348 try: 349 # 체인지 리스트의 모든 파일 되돌리기 350 self.p4.run_revert("-c", change_list_number, "//...") 351 logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.") 352 353 # 빈 체인지 리스트 삭제 354 try: 355 self.p4.run_change("-d", change_list_number) 356 logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.") 357 except P4Exception as e_delete: 358 self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제") 359 logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.") 360 return False 361 362 return True 363 except P4Exception as e: 364 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기") 365 return False
체인지 리스트를 되돌리고 삭제합니다.
체인지 리스트 내 모든 파일을 되돌린 후 빈 체인지 리스트를 삭제합니다.
Args: change_list_number (int): 되돌릴 체인지 리스트 번호
Returns: bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False
367 def delete_empty_change_list(self, change_list_number: int) -> bool: 368 """빈 체인지 리스트를 삭제합니다. 369 370 Args: 371 change_list_number (int): 삭제할 체인지 리스트 번호 372 373 Returns: 374 bool: 삭제 성공 시 True, 실패 시 False 375 """ 376 if not self._is_connected(): 377 return False 378 379 logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...") 380 try: 381 # 체인지 리스트 정보 가져오기 382 change_spec = self.p4.fetch_change(change_list_number) 383 384 # 파일이 있는지 확인 385 if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0: 386 logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.") 387 return False 388 389 # 빈 체인지 리스트 삭제 390 self.p4.run_change("-d", change_list_number) 391 logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.") 392 return True 393 except P4Exception as e: 394 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제") 395 return False
빈 체인지 리스트를 삭제합니다.
Args: change_list_number (int): 삭제할 체인지 리스트 번호
Returns: bool: 삭제 성공 시 True, 실패 시 False
397 def revert_files(self, change_list_number: int, file_paths: list) -> bool: 398 """체인지 리스트 내의 특정 파일들을 되돌립니다. 399 400 Args: 401 change_list_number (int): 체인지 리스트 번호 402 file_paths (list): 되돌릴 파일 경로 리스트 403 404 Returns: 405 bool: 되돌리기 성공 시 True, 실패 시 False 406 """ 407 if not self._is_connected(): 408 return False 409 if not file_paths: 410 logger.warning("되돌릴 파일 목록이 비어있습니다.") 411 return True 412 413 logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...") 414 try: 415 for file_path in file_paths: 416 self.p4.run_revert("-c", change_list_number, file_path) 417 logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.") 418 return True 419 except P4Exception as e: 420 self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기") 421 return False
체인지 리스트 내의 특정 파일들을 되돌립니다.
Args: change_list_number (int): 체인지 리스트 번호 file_paths (list): 되돌릴 파일 경로 리스트
Returns: bool: 되돌리기 성공 시 True, 실패 시 False
423 def check_update_required(self, file_paths: list) -> bool: 424 """파일이나 폴더의 업데이트 필요 여부를 확인합니다. 425 426 Args: 427 file_paths (list): 확인할 파일 또는 폴더 경로 리스트. 428 폴더 경로는 자동으로 재귀적으로 처리됩니다. 429 430 Returns: 431 bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False 432 """ 433 if not self._is_connected(): 434 return False 435 if not file_paths: 436 logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.") 437 return False 438 439 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 440 processed_paths = [] 441 for path in file_paths: 442 if os.path.isdir(path): 443 # 폴더 경로에 '...'(재귀) 패턴을 추가 444 processed_paths.append(os.path.join(path, '...')) 445 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 446 else: 447 processed_paths.append(path) 448 449 logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}") 450 try: 451 sync_preview_results = self.p4.run_sync("-n", processed_paths) 452 needs_update = False 453 for result in sync_preview_results: 454 if isinstance(result, dict): 455 if 'up-to-date' not in result.get('how', '') and \ 456 'no such file(s)' not in result.get('depotFile', ''): 457 if result.get('how') and 'syncing' in result.get('how'): 458 needs_update = True 459 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}") 460 break 461 elif result.get('action') and result.get('action') not in ['checked', 'exists']: 462 needs_update = True 463 logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})") 464 break 465 elif isinstance(result, str): 466 if "up-to-date" not in result and "no such file(s)" not in result: 467 needs_update = True 468 logger.info(f"파일 업데이트 필요 (문자열 결과): {result}") 469 break 470 471 if needs_update: 472 logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.") 473 else: 474 logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.") 475 return needs_update 476 except P4Exception as e: 477 self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})") 478 return False
파일이나 폴더의 업데이트 필요 여부를 확인합니다.
Args: file_paths (list): 확인할 파일 또는 폴더 경로 리스트. 폴더 경로는 자동으로 재귀적으로 처리됩니다.
Returns: bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False
480 def sync_files(self, file_paths: list) -> bool: 481 """파일이나 폴더를 동기화합니다. 482 483 Args: 484 file_paths (list): 동기화할 파일 또는 폴더 경로 리스트. 485 폴더 경로는 자동으로 재귀적으로 처리됩니다. 486 487 Returns: 488 bool: 동기화 성공 시 True, 실패 시 False 489 """ 490 if not self._is_connected(): 491 return False 492 if not file_paths: 493 logger.debug("싱크할 파일/폴더 목록이 비어있습니다.") 494 return True 495 496 # 폴더 경로에 재귀적 와일드카드 패턴을 추가 497 processed_paths = [] 498 for path in file_paths: 499 if os.path.isdir(path): 500 # 폴더 경로에 '...'(재귀) 패턴을 추가 501 processed_paths.append(os.path.join(path, '...')) 502 logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}") 503 else: 504 processed_paths.append(path) 505 506 logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}") 507 try: 508 self.p4.run_sync(processed_paths) 509 logger.info(f"파일/폴더 싱크 완료: {processed_paths}") 510 return True 511 except P4Exception as e: 512 self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})") 513 return False
파일이나 폴더를 동기화합니다.
Args: file_paths (list): 동기화할 파일 또는 폴더 경로 리스트. 폴더 경로는 자동으로 재귀적으로 처리됩니다.
Returns: bool: 동기화 성공 시 True, 실패 시 False
515 def disconnect(self): 516 """Perforce 서버와의 연결을 해제합니다.""" 517 if self.connected: 518 try: 519 self.p4.disconnect() 520 self.connected = False 521 logger.info("Perforce 서버 연결 해제 완료.") 522 except P4Exception as e: 523 self._handle_p4_exception(e, "Perforce 서버 연결 해제") 524 else: 525 logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.")
Perforce 서버와의 연결을 해제합니다.