pyjallib.p4module API documentation

# pyjallib.p4module

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", '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()

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

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

P4Module() View Source

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

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

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

    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

def get_pending_change_list(self) -> list: View Source

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

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

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

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

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

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

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

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

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

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

    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

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

    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

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

    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

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

    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

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

    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

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

    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

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

    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

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

    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

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

    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

def disconnect(self): View Source

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