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