pyjallib.p4module

P4Python을 사용하는 Perforce 모듈.

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

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