pyjallib.perforce API documentation

# pyjallib.perforce

P4Python을 사용하는 Perforce 모듈.

이 모듈은 P4Python을 사용하여 Perforce 서버와 상호작용하는 기능을 제공합니다. 주요 기능:

- 워크스페이스 연결
- 체인지리스트 관리 (생성, 조회, 편집, 제출, 되돌리기)
- 파일 작업 (체크아웃, 추가, 삭제)
- 파일 동기화 및 업데이트 확인

View Source

      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()

logger = <Logger pyjallib.perforce (DEBUG)>
log_path = 'C:\\Users\\jalnaga\\Documents\\Perforce.log'
file_handler = <FileHandler C:\Users\jalnaga\Documents\Perforce.log (NOTSET)>
class Perforce: View Source

     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 작업을 수행하는 클래스.

Perforce() View Source

    31    def __init__(self):
    32        """Perforce 인스턴스를 초기화합니다."""
    33        self.p4 = P4()
    34        self.connected = False
    35        self.workspaceRoot = r""
    36        logger.info("Perforce 인스턴스 생성됨")

Perforce 인스턴스를 초기화합니다.

p4
connected
workspaceRoot
def connect(self, workspace_name: str) -> bool: View Source

    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

def get_pending_change_list(self) -> list: View Source

     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: 체인지 리스트 정보 딕셔너리들의 리스트

def create_change_list(self, description: str) -> dict: View Source

    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: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리

def get_change_list_by_number(self, change_list_number: int) -> dict: View Source

    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: 체인지 리스트 정보. 실패 시 빈 딕셔너리

def get_change_list_by_description(self, description: str) -> dict: View Source

    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: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트)

def edit_change_list( self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict: View Source

    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: 업데이트된 체인지 리스트 정보

def checkout_file(self, file_path: str, change_list_number: int) -> bool: View Source

    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

def add_file(self, file_path: str, change_list_number: int) -> bool: View Source

    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

def delete_file(self, file_path: str, change_list_number: int) -> bool: View Source

    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

def submit_change_list(self, change_list_number: int) -> bool: View Source

    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

def revert_change_list(self, change_list_number: int) -> bool: View Source

    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

def delete_empty_change_list(self, change_list_number: int) -> bool: View Source

    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

def revert_files(self, change_list_number: int, file_paths: list) -> bool: View Source

    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

def check_update_required(self, file_paths: list) -> bool: View Source

    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

def sync_files(self, file_paths: list) -> bool: View Source

    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

def disconnect(self): View Source

    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 서버와의 연결을 해제합니다.