pyjallib 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyjallib/perforce.py CHANGED
@@ -1,735 +1,528 @@
1
+ """
2
+ P4Python을 사용하는 Perforce 모듈.
3
+
4
+ 이 모듈은 P4Python을 사용하여 Perforce 서버와 상호작용하는 기능을 제공합니다.
5
+ 주요 기능:
6
+ - 워크스페이스 연결
7
+ - 체인지리스트 관리 (생성, 조회, 편집, 제출, 되돌리기)
8
+ - 파일 작업 (체크아웃, 추가, 삭제)
9
+ - 파일 동기화 및 업데이트 확인
10
+ """
11
+
12
+ import logging
13
+ from P4 import P4, P4Exception
1
14
  import os
2
- import subprocess
3
- import socket
15
+ from pathlib import Path
4
16
 
5
- class Perforce:
6
- """
7
- Perforce 버전 관리 시스템과의 상호작용을 위한 클래스입니다.
8
-
9
- 클래스는 Perforce 명령을 실행하고, 워크스페이스를 관리하며
10
- Perforce 서버와의 연결을 제어하는 기능을 제공합니다.
11
- """
12
-
13
- def __init__(self, server, user, workspace=None):
14
- """
15
- Perforce 클래스의 인스턴스를 초기화합니다.
16
-
17
- Parameters:
18
- server (str, optional): Perforce 서버 주소. 기본값은 환경 변수 P4PORT 또는 "PC-BUILD:1666"
19
- user (str, optional): Perforce 사용자 이름. 기본값은 환경 변수 P4USER 또는 "Dev"
20
- workspace (str, optional): Perforce 워크스페이스 이름. 기본값은 환경 변수 P4CLIENT
21
- """
22
- self.server = server
23
- self.user = user
24
- self.workspace = workspace if workspace else os.environ.get('P4CLIENT')
25
- self.workspaceRoot = None
26
- self.localHostName = socket.gethostname()
27
-
28
- os.environ['P4USER'] = self.user
29
- os.environ['P4PORT'] = self.server
30
- if self.workspace:
31
- os.environ['P4CLIENT'] = self.workspace
32
- else:
33
- # P4CLIENT가 None이면 환경 변수에서 제거 시도 (선택적)
34
- if 'P4CLIENT' in os.environ:
35
- del os.environ['P4CLIENT']
36
-
37
- # 초기화 시 연결 확인
38
- self._initialize_connection()
39
-
40
- def _initialize_connection(self):
41
- """
42
- Perforce 서버와의 연결을 초기화합니다.
43
-
44
- 서버 연결을 확인하고 워크스페이스 루트 경로를 설정합니다.
45
-
46
- Returns:
47
- str: Perforce 서버 정보 문자열
48
-
49
- Raises:
50
- Exception: Perforce 연결 초기화 실패 시 예외 처리
51
- """
52
- result = None
53
- try:
54
- # 서버 연결 확인 (info 명령은 가볍고 빠르게 실행됨)
55
- result = subprocess.run(['p4', 'info'],
56
- capture_output=True,
57
- text=True,
58
- encoding="utf-8")
59
-
60
- workSpaceRootPathResult = subprocess.run(
61
- ['p4', '-F', '%clientRoot%', '-ztag', 'info'],
62
- capture_output=True,
63
- text=True,
64
- encoding="utf-8"
65
- ).stdout.strip()
66
- self.workspaceRoot = os.path.normpath(workSpaceRootPathResult)
67
-
68
- if result.returncode != 0:
69
- print(f"Perforce 초기화 중 경고: {result.stderr}")
70
- except Exception as e:
71
- print(f"Perforce 초기화 실패: {e}")
72
-
73
- return result.stdout.strip()
74
-
75
- def _run_command(self, inCommands):
76
- """
77
- Perforce 명령을 실행하고 결과를 반환합니다.
78
-
79
- Parameters:
80
- inCommands (list): 실행할 Perforce 명령어와 인수들의 리스트
81
-
82
- Returns:
83
- str: 명령 실행 결과 문자열
84
- """
85
- self._initialize_connection()
86
-
87
- commands = ['p4'] + inCommands
88
- result = subprocess.run(commands, capture_output=True, text=True, encoding="utf-8")
89
-
90
- return result.stdout.strip()
17
+ # 로깅 설정
18
+ logger = logging.getLogger(__name__)
19
+ logger.setLevel(logging.DEBUG)
20
+ # 사용자 문서 폴더 내 로그 파일 저장
21
+ log_path = os.path.join(Path.home() / "Documents", 'Perforce.log')
22
+ file_handler = logging.FileHandler(log_path, encoding='utf-8')
23
+ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
24
+ logger.addHandler(file_handler)
91
25
 
92
- def get_local_hostname(self):
93
- """
94
- 현재 로컬 머신의 호스트 이름을 반환합니다.
95
-
96
- Returns:
97
- str: 로컬 머신의 호스트 이름
98
- """
99
- # 현재 로컬 머신의 호스트 이름을 반환합니다.
100
- return self.localHostName
101
26
 
102
- def get_all_clients(self):
103
- """
104
- 모든 Perforce 클라이언트 워크스페이스의 이름 목록을 반환합니다.
105
-
106
- Returns:
107
- list: 클라이언트 워크스페이스 이름 리스트
108
- """
109
- # 모든 클라이언트 워크스페이스의 이름을 반환합니다.
110
- result = self._run_command(['clients'])
111
- clients = []
112
-
113
- if result is None:
114
- return clients
115
-
116
- for line in result.splitlines():
117
- if line.startswith('Client'):
118
- parts = line.split()
119
- if len(parts) >= 2:
120
- clients.append(parts[1])
121
- return clients
122
-
123
- def get_local_workspaces(self):
124
- """
125
- 현재 로컬 머신에 있는 워크스페이스 목록을 반환합니다.
126
-
127
- 현재 호스트 이름으로 시작하는 모든 클라이언트를 찾습니다.
128
-
129
- Returns:
130
- list: 로컬 머신의 워크스페이스 이름 리스트
131
- """
132
- all_clients = self.get_all_clients()
133
- local_clients = []
27
+ class Perforce:
28
+ """P4Python을 사용하여 Perforce 작업을 수행하는 클래스."""
134
29
 
135
- for client in all_clients:
136
- if client.startswith(self.localHostName):
137
- local_clients.append(client)
30
+ def __init__(self):
31
+ """Perforce 인스턴스를 초기화합니다."""
32
+ self.p4 = P4()
33
+ self.connected = False
34
+ self.workspaceRoot = r""
35
+ logger.info("Perforce 인스턴스 생성됨")
36
+
37
+ def _is_connected(self) -> bool:
38
+ """Perforce 서버 연결 상태를 확인합니다.
138
39
 
139
- return local_clients
140
-
141
- def set_workspace(self, inWorkspace):
142
- """
143
- 주어진 워크스페이스로 현재 작업 환경을 전환합니다.
144
-
145
- Parameters:
146
- inWorkspace (str): 전환할 워크스페이스 이름
147
-
148
40
  Returns:
149
- str: 워크스페이스 정보 문자열
150
-
151
- Raises:
152
- ValueError: 지정된 워크스페이스가 로컬 워크스페이스 목록에 없을 경우
41
+ bool: 연결되어 있으면 True, 아니면 False
153
42
  """
154
- # 주어진 워크스페이스로 전환합니다.
155
- localWorkSpaces = self.get_local_workspaces()
156
- if inWorkspace not in localWorkSpaces:
157
- print(f"워크스페이스 '{inWorkspace}'는 로컬 워크스페이스 목록에 없습니다.")
43
+ if not self.connected:
44
+ logger.warning("Perforce 서버에 연결되지 않았습니다.")
158
45
  return False
159
-
160
- self.workspace = inWorkspace
161
- os.environ['P4CLIENT'] = self.workspace
162
-
163
46
  return True
164
-
165
- def sync(self, inWorkSpace=None, inPaths=None):
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): 예외가 발생한 컨텍스트 설명
166
54
  """
167
- Perforce 워크스페이스를 최신 버전으로 동기화합니다.
168
-
169
- Parameters:
170
- inWorkSpace (str, optional): 동기화할 워크스페이스 이름
171
- inPaths (str or list, optional): 동기화할 특정 경로 또는 경로 목록
172
-
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
+
173
67
  Returns:
174
- bool: 동기화 성공 여부
68
+ bool: 연결 성공 시 True, 실패 시 False
175
69
  """
176
- # 주어진 워크스페이스를 동기화합니다.
177
- if inWorkSpace == None:
178
- if self.workspace == None:
179
- print(f"워크스페이스가 없습니다.")
180
- return False
181
- else:
182
- if not self.set_workspace(inWorkSpace):
183
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
184
- return False
185
-
186
- # 동기화 명령 실행
187
- sync_command = ['sync']
188
-
189
- if inPaths:
190
- # 문자열로 단일 경로가 주어진 경우 리스트로 변환
191
- if isinstance(inPaths, str):
192
- inPaths = [inPaths]
193
-
194
- valid_paths = []
195
- for path in inPaths:
196
- if os.path.exists(path):
197
- valid_paths.append(path)
198
- else:
199
- print(f"경고: 지정된 경로 '{path}'가 로컬에 존재하지 않습니다.")
70
+ logger.info(f"'{workspace_name}' 워크스페이스에 연결 시도 중...")
71
+ try:
72
+ self.p4.client = workspace_name
73
+ self.p4.connect()
74
+ self.connected = True
200
75
 
201
- if not valid_paths:
202
- print("유효한 경로가 없어 동기화를 진행할 수 없습니다.")
203
- return False
76
+ # 워크스페이스 루트 경로 가져오기
77
+ try:
78
+ client_info = self.p4.run_client("-o", workspace_name)[0]
79
+ root_path = client_info.get("Root", "")
204
80
 
205
- # 유효한 경로들을 모두 동기화 명령에 추가
206
- sync_command.extend(valid_paths)
207
-
208
- self._run_command(sync_command)
209
- return True
210
-
211
- def get_changelists(self, inWorkSpace=None):
212
- """
213
- 특정 워크스페이스의 pending 상태 체인지 리스트를 가져옵니다.
214
-
215
- Parameters:
216
- inWorkSpace (str, optional): 체인지 리스트를 가져올 워크스페이스 이름
217
-
81
+ # Windows 경로 형식으로 변환 (슬래시를 백슬래시로)
82
+ root_path = os.path.normpath(root_path)
83
+
84
+ self.workspaceRoot = root_path
85
+ logger.info(f"워크스페이스 루트 절대 경로: {self.workspaceRoot}")
86
+ except (IndexError, KeyError) as e:
87
+ logger.error(f"워크스페이스 루트 경로 가져오기 실패: {e}")
88
+ self.workspaceRoot = ""
89
+
90
+ logger.info(f"'{workspace_name}' 워크스페이스에 성공적으로 연결됨 (User: {self.p4.user}, Port: {self.p4.port})")
91
+ return True
92
+ except P4Exception as e:
93
+ self.connected = False
94
+ self._handle_p4_exception(e, f"'{workspace_name}' 워크스페이스 연결")
95
+ return False
96
+
97
+ def get_pending_change_list(self) -> list:
98
+ """워크스페이스의 Pending된 체인지 리스트를 가져옵니다.
99
+
218
100
  Returns:
219
- list: 체인지 리스트 정보 딕셔너리의 리스트
101
+ list: 체인지 리스트 정보 딕셔너리들의 리스트
220
102
  """
221
- if inWorkSpace == None:
222
- if self.workspace == None:
223
- print(f"워크스페이스가 없습니다.")
224
- return []
225
- else:
226
- if not self.set_workspace(inWorkSpace):
227
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
228
- return []
229
-
230
- # 체인지 리스트 명령 실행
231
- changes_command = ['changes']
232
-
233
- # 항상 pending 상태만 가져오도록 설정
234
- changes_command.extend(['-s', 'pending'])
103
+ if not self._is_connected():
104
+ return []
105
+ logger.debug("Pending 체인지 리스트 조회 중...")
106
+ try:
107
+ pending_changes = self.p4.run_changes("-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
108
+ change_numbers = [int(cl['change']) for cl in pending_changes]
235
109
 
236
- if self.workspace:
237
- changes_command.extend(['-c', self.workspace])
238
-
239
- result = self._run_command(changes_command)
240
- changes = []
241
-
242
- for line in result.splitlines():
243
- if line.startswith('Change'):
244
- parts = line.split()
245
- if len(parts) >= 5:
246
- change_id = parts[1]
247
-
248
- # 설명 부분 추출
249
- desc_start = line.find("'")
250
- desc_end = line.rfind("'")
251
- description = line[desc_start+1:desc_end] if desc_start != -1 and desc_end != -1 else ""
252
-
253
- changes.append({
254
- 'id': change_id,
255
- 'description': description
256
- })
257
-
258
- return changes
259
-
260
- def create_new_changelist(self, inDescription="Created by pyjallib", inWorkSpace=None):
261
- """
262
- 새로운 체인지 리스트를 생성합니다.
263
-
264
- Parameters:
265
- inDescription (str): 체인지 리스트 설명
266
- inWorkSpace (str, optional): 체인지 리스트를 생성할 워크스페이스 이름
110
+ # 각 체인지 리스트 번호에 대한 상세 정보 가져오기
111
+ change_list_info = []
112
+ for change_number in change_numbers:
113
+ cl_info = self.get_change_list_by_number(change_number)
114
+ if cl_info:
115
+ change_list_info.append(cl_info)
267
116
 
117
+ logger.info(f"Pending 체인지 리스트 {len(change_list_info)}개 조회 완료")
118
+ return change_list_info
119
+ except P4Exception as e:
120
+ self._handle_p4_exception(e, "Pending 체인지 리스트 조회")
121
+ return []
122
+
123
+ def create_change_list(self, description: str) -> dict:
124
+ """새로운 체인지 리스트를 생성합니다.
125
+
126
+ Args:
127
+ description (str): 체인지 리스트 설명
128
+
268
129
  Returns:
269
- dict: 생성된 체인지 리스트 정보 {'id': str, 'description': str} 또는 실패 시 None
130
+ dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리
270
131
  """
271
- if inWorkSpace == None:
272
- if self.workspace == None:
273
- print(f"워크스페이스가 없습니다.")
274
- return None
275
- else:
276
- if not self.set_workspace(inWorkSpace):
277
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
278
- return None
279
-
280
- # 체인지 리스트 생성 명령 실행
281
- change_command = ['change', '-o']
282
- result = self._run_command(change_command)
283
-
284
- # 체인지 스펙 수정
285
- modified_spec = []
286
- for line in result.splitlines():
287
- if line.startswith('Description:'):
288
- modified_spec.append(line)
289
- # Add the new description, handling potential multi-line descriptions correctly
290
- for desc_line in inDescription.splitlines():
291
- modified_spec.append(f"\t{desc_line}")
292
- # Skip the default empty tab line if present
293
- # This assumes the default spec has an empty line after Description:
294
- # If not, this logic might need adjustment based on actual 'p4 change -o' output
295
- # We'll rely on the loop structure to handle subsequent lines correctly.
296
- continue # Move to the next line in the original spec
297
- # Only append lines that are not part of the old description placeholder
298
- # This logic assumes the default description is just a placeholder like '<enter description here>'
299
- # or similar, often on a single line after 'Description:'.
300
- # A more robust approach might be needed if the default spec is complex.
301
- if not (line.startswith('\t') and 'Description:' in modified_spec[-1]):
302
- modified_spec.append(line)
303
-
304
- # 수정된 체인지 스펙으로 체인지 리스트 생성
305
- create_result = subprocess.run(['p4', 'change', '-i'],
306
- input='\n'.join(modified_spec),
307
- capture_output=True,
308
- text=True,
309
- encoding="utf-8")
310
-
311
- # 결과에서 체인지 리스트 ID 추출
312
- if create_result.returncode == 0 and create_result.stdout:
313
- output = create_result.stdout.strip()
314
- # 예: "Change 12345 created."
315
- if 'Change' in output and 'created' in output:
316
- parts = output.split()
317
- if len(parts) >= 2:
318
- change_id = parts[1]
319
- return {'id': change_id, 'description': inDescription} # Return dictionary
320
-
321
- print(f"Failed to create changelist. Error: {create_result.stderr}") # Log error if creation failed
322
- return None
323
-
324
- def checkout_files(self, inFiles, inChangelist=None, inWorkSpace=None):
325
- """
326
- 지정한 파일들을 체크아웃하고 특정 체인지 리스트에 추가합니다.
327
-
328
- Parameters:
329
- inFiles (list): 체크아웃할 파일 경로 리스트
330
- inChangelist (str, optional): 파일을 추가할 체인지 리스트 ID
331
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
332
-
132
+ if not self._is_connected():
133
+ return {}
134
+ logger.info(f" 체인지 리스트 생성 시도: '{description}'")
135
+ try:
136
+ change_spec = self.p4.fetch_change()
137
+ change_spec["Description"] = description
138
+ result = self.p4.save_change(change_spec)
139
+ created_change_number = int(result[0].split()[1])
140
+ logger.info(f"체인지 리스트 {created_change_number} 생성 완료: '{description}'")
141
+ return self.get_change_list_by_number(created_change_number)
142
+ except P4Exception as e:
143
+ self._handle_p4_exception(e, f"체인지 리스트 생성 ('{description}')")
144
+ return {}
145
+ except (IndexError, ValueError) as e:
146
+ logger.error(f"체인지 리스트 번호 파싱 오류: {e}")
147
+ return {}
148
+
149
+ def get_change_list_by_number(self, change_list_number: int) -> dict:
150
+ """체인지 리스트 번호로 체인지 리스트를 가져옵니다.
151
+
152
+ Args:
153
+ change_list_number (int): 체인지 리스트 번호
154
+
333
155
  Returns:
334
- bool: 성공 여부
156
+ dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리
335
157
  """
336
- if inWorkSpace == None:
337
- if self.workspace == None:
338
- print(f"워크스페이스가 없습니다.")
339
- return False
340
- else:
341
- if not self.set_workspace(inWorkSpace):
342
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
343
- return False
344
-
345
- if not inFiles:
346
- return False
347
-
348
- # 체크아웃 명령 실행
349
- edit_command = ['edit']
350
-
351
- target_changelist = inChangelist
352
- if not target_changelist:
353
- new_changelist_info = self.create_new_changelist(inDescription=f"Auto-checkout for {len(inFiles)} files")
354
- if not new_changelist_info:
355
- print("Failed to create a new changelist for checkout.")
356
- return False
357
- target_changelist = new_changelist_info['id']
158
+ if not self._is_connected():
159
+ return {}
160
+ logger.debug(f"체인지 리스트 {change_list_number} 정보 조회 중...")
161
+ try:
162
+ cl_info = self.p4.fetch_change(change_list_number)
163
+ if cl_info:
164
+ logger.info(f"체인지 리스트 {change_list_number} 정보 조회 완료.")
165
+ return cl_info
166
+ else:
167
+ logger.warning(f"체인지 리스트 {change_list_number}를 찾을 수 없습니다.")
168
+ return {}
169
+ except P4Exception as e:
170
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 정보 조회")
171
+ return {}
172
+
173
+ def get_change_list_by_description(self, description: str) -> dict:
174
+ """체인지 리스트 설명으로 체인지 리스트를 가져옵니다.
175
+
176
+ Args:
177
+ description (str): 체인지 리스트 설명
358
178
 
359
- edit_command.extend(['-c', target_changelist])
360
- edit_command.extend(inFiles)
361
-
362
- result = self._run_command(edit_command)
363
-
364
- if len(self.get_changelist_files(target_changelist)) == 0:
365
- self.delete_changelist(target_changelist)
366
-
367
- return True
368
-
369
- def add_files(self, inFiles, inChangelist=None, inWorkSpace=None):
370
- """
371
- 지정한 파일들을 Perforce에 추가합니다.
372
-
373
- Parameters:
374
- inFiles (list): 추가할 파일 경로 리스트
375
- inChangelist (str, optional): 파일을 추가할 체인지 리스트 ID
376
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
377
-
378
179
  Returns:
379
- bool: 성공 여부
180
+ dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트)
380
181
  """
381
- if inWorkSpace == None:
382
- if self.workspace == None:
383
- print(f"워크스페이스가 없습니다.")
384
- return False
385
- else:
386
- if not self.set_workspace(inWorkSpace):
387
- print(f"워크스페이스 '{inWorkSpace}' 로컬 워크스페이스 목록에 없습니다.")
388
- return False
389
-
390
- if not inFiles:
391
- return False
392
-
393
- # 파일 추가 명령 실행
394
- add_command = ['add']
395
-
396
- target_changelist = inChangelist
397
- if not target_changelist:
398
- new_changelist_info = self.create_new_changelist(inDescription=f"Auto-add for {len(inFiles)} files")
399
- if not new_changelist_info:
400
- print("Failed to create a new changelist for add.")
401
- return False
402
- target_changelist = new_changelist_info['id']
182
+ if not self._is_connected():
183
+ return {}
184
+ logger.debug(f"설명으로 체인지 리스트 조회 중: '{description}'")
185
+ try:
186
+ pending_changes = self.p4.run_changes("-l", "-s", "pending", "-u", self.p4.user, "-c", self.p4.client)
187
+ for cl in pending_changes:
188
+ cl_desc = cl.get('Description', b'').decode('utf-8', 'replace').strip()
189
+ if cl_desc == description.strip():
190
+ logger.info(f"설명 '{description}'에 해당하는 체인지 리스트 {cl['change']} 조회 완료.")
191
+ return self.get_change_list_by_number(int(cl['change']))
192
+ logger.info(f"설명 '{description}'에 해당하는 Pending 체인지 리스트를 찾을 수 없습니다.")
193
+ return {}
194
+ except P4Exception as e:
195
+ self._handle_p4_exception(e, f"설명으로 체인지 리스트 조회 ('{description}')")
196
+ return {}
197
+
198
+ def edit_change_list(self, change_list_number: int, description: str = None, add_file_paths: list = None, remove_file_paths: list = None) -> dict:
199
+ """체인지 리스트를 편집합니다.
200
+
201
+ Args:
202
+ change_list_number (int): 체인지 리스트 번호
203
+ description (str, optional): 변경할 설명
204
+ add_file_paths (list, optional): 추가할 파일 경로 리스트
205
+ remove_file_paths (list, optional): 제거할 파일 경로 리스트
403
206
 
404
- add_command.extend(['-c', target_changelist])
405
- add_command.extend(inFiles)
406
-
407
- result = self._run_command(add_command)
408
-
409
- if len(self.get_changelist_files(target_changelist)) == 0:
410
- self.delete_changelist(target_changelist)
411
-
412
- return True
413
-
414
- def delete_files(self, inFiles, inChangelist=None, inWorkSpace=None):
207
+ Returns:
208
+ dict: 업데이트된 체인지 리스트 정보
415
209
  """
416
- 지정한 파일들을 Perforce에서 삭제합니다.
417
-
418
- Parameters:
419
- inFiles (list): 삭제할 파일 경로 리스트
420
- inChangelist (str, optional): 파일 삭제를 추가할 체인지 리스트 ID
421
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
210
+ if not self._is_connected():
211
+ return {}
212
+ logger.info(f"체인지 리스트 {change_list_number} 편집 시도...")
213
+ try:
214
+ if description is not None:
215
+ change_spec = self.p4.fetch_change(change_list_number)
216
+ current_description = change_spec.get('Description', '').strip()
217
+ if current_description != description.strip():
218
+ change_spec['Description'] = description
219
+ self.p4.save_change(change_spec)
220
+ logger.info(f"체인지 리스트 {change_list_number} 설명 변경 완료: '{description}'")
221
+
222
+ if add_file_paths:
223
+ for file_path in add_file_paths:
224
+ try:
225
+ self.p4.run_reopen("-c", change_list_number, file_path)
226
+ logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}로 이동 완료.")
227
+ except P4Exception as e_reopen:
228
+ self._handle_p4_exception(e_reopen, f"파일 '{file_path}'을 CL {change_list_number}로 이동")
229
+
230
+ if remove_file_paths:
231
+ for file_path in remove_file_paths:
232
+ try:
233
+ self.p4.run_revert("-c", change_list_number, file_path)
234
+ logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 제거(revert) 완료.")
235
+ except P4Exception as e_revert:
236
+ self._handle_p4_exception(e_revert, f"파일 '{file_path}'을 CL {change_list_number}에서 제거(revert)")
237
+
238
+ return self.get_change_list_by_number(change_list_number)
239
+
240
+ except P4Exception as e:
241
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 편집")
242
+ return self.get_change_list_by_number(change_list_number)
243
+
244
+ def _file_op(self, command: str, file_path: str, change_list_number: int, op_name: str) -> bool:
245
+ """파일 작업을 수행하는 내부 헬퍼 함수입니다.
246
+
247
+ Args:
248
+ command (str): 실행할 명령어 (edit/add/delete)
249
+ file_path (str): 대상 파일 경로
250
+ change_list_number (int): 체인지 리스트 번호
251
+ op_name (str): 작업 이름 (로깅용)
422
252
 
423
253
  Returns:
424
- bool: 성공 여부
254
+ bool: 작업 성공 시 True, 실패 시 False
425
255
  """
426
- if inWorkSpace == None:
427
- if self.workspace == None:
428
- print(f"워크스페이스가 없습니다.")
429
- return False
430
- else:
431
- if not self.set_workspace(inWorkSpace):
432
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
256
+ if not self._is_connected():
257
+ return False
258
+ logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 시도 (CL: {change_list_number})...")
259
+ try:
260
+ if command == "edit":
261
+ self.p4.run_edit("-c", change_list_number, file_path)
262
+ elif command == "add":
263
+ self.p4.run_add("-c", change_list_number, file_path)
264
+ elif command == "delete":
265
+ self.p4.run_delete("-c", change_list_number, file_path)
266
+ else:
267
+ logger.error(f"지원되지 않는 파일 작업: {command}")
433
268
  return False
434
-
435
- if not inFiles:
269
+ logger.info(f"파일 '{file_path}'에 대한 '{op_name}' 작업 성공 (CL: {change_list_number}).")
270
+ return True
271
+ except P4Exception as e:
272
+ self._handle_p4_exception(e, f"파일 '{file_path}' {op_name} (CL: {change_list_number})")
436
273
  return False
437
274
 
438
- # 파일 삭제 명령 실행
439
- delete_command = ['delete']
440
-
441
- target_changelist = inChangelist
442
- if not target_changelist:
443
- # If no changelist is specified, create a new one
444
- new_changelist_info = self.create_new_changelist(inDescription=f"Auto-delete for {len(inFiles)} files")
445
- if not new_changelist_info:
446
- print("Failed to create a new changelist for delete.")
447
- return False
448
- target_changelist = new_changelist_info['id']
275
+ def checkout_file(self, file_path: str, change_list_number: int) -> bool:
276
+ """파일을 체크아웃합니다.
449
277
 
450
- delete_command.extend(['-c', target_changelist])
451
- delete_command.extend(inFiles)
278
+ Args:
279
+ file_path (str): 체크아웃할 파일 경로
280
+ change_list_number (int): 체인지 리스트 번호
452
281
 
453
- result = self._run_command(delete_command)
454
-
455
- if len(self.get_changelist_files(target_changelist)) == 0:
456
- self.delete_changelist(target_changelist)
457
-
458
- return True
459
-
460
- def revert_changelist(self, inChangelist, inWorkSpace=None):
282
+ Returns:
283
+ bool: 체크아웃 성공 시 True, 실패 시 False
461
284
  """
462
- 특정 체인지 리스트의 모든 변경 사항을 되돌립니다 (revert).
285
+ return self._file_op("edit", file_path, change_list_number, "체크아웃")
463
286
 
464
- Parameters:
465
- inChangelist (str): 되돌릴 체인지 리스트 ID
466
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
287
+ def add_file(self, file_path: str, change_list_number: int) -> bool:
288
+ """파일을 추가합니다.
289
+
290
+ Args:
291
+ file_path (str): 추가할 파일 경로
292
+ change_list_number (int): 체인지 리스트 번호
467
293
 
468
294
  Returns:
469
- bool: 성공 여부
295
+ bool: 추가 성공 시 True, 실패 시 False
470
296
  """
471
- if inWorkSpace == None:
472
- if self.workspace == None:
473
- print(f"워크스페이스가 없습니다.")
474
- return False
475
- else:
476
- if not self.set_workspace(inWorkSpace):
477
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
478
- return False
479
-
480
- if not inChangelist:
481
- print("Error: Changelist ID must be provided for revert operation.")
482
- return False
297
+ return self._file_op("add", file_path, change_list_number, "추가")
483
298
 
484
- # Revert 명령 실행
485
- revert_command = ['revert', '-c', inChangelist, '//...'] # Revert all files in the changelist
299
+ def delete_file(self, file_path: str, change_list_number: int) -> bool:
300
+ """파일을 삭제합니다.
486
301
 
487
- result = self._run_command(revert_command)
488
- # p4 revert might not return an error code even if nothing was reverted.
489
- # We'll assume success if the command ran. More robust checking might involve parsing 'result'.
490
- print(f"Revert result for CL {inChangelist}:\n{result}")
491
- self._run_command(['change', '-d', inChangelist])
492
- return True
302
+ Args:
303
+ file_path (str): 삭제할 파일 경로
304
+ change_list_number (int): 체인지 리스트 번호
493
305
 
494
- def submit_changelist(self, inChangelist, inDescription=None, inWorkSpace=None):
306
+ Returns:
307
+ bool: 삭제 성공 시 True, 실패 시 False
495
308
  """
496
- 특정 체인지 리스트를 서버에 제출합니다.
309
+ return self._file_op("delete", file_path, change_list_number, "삭제")
310
+
311
+ def submit_change_list(self, change_list_number: int) -> bool:
312
+ """체인지 리스트를 제출합니다.
497
313
 
498
- Parameters:
499
- inChangelist (str): 제출할 체인지 리스트 ID
500
- inDescription (str, optional): 제출 설명 (없으면 체인지 리스트의 기존 설명 사용)
501
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
314
+ Args:
315
+ change_list_number (int): 제출할 체인지 리스트 번호
502
316
 
503
317
  Returns:
504
- bool: 성공 여부
318
+ bool: 제출 성공 시 True, 실패 시 False
505
319
  """
506
- if inWorkSpace == None:
507
- if self.workspace == None:
508
- print(f"워크스페이스가 없습니다.")
509
- return False
510
- else:
511
- if not self.set_workspace(inWorkSpace):
512
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
513
- return False
514
-
515
- if not inChangelist:
320
+ if not self._is_connected():
321
+ return False
322
+ logger.info(f"체인지 리스트 {change_list_number} 제출 시도...")
323
+ try:
324
+ self.p4.run_submit("-c", change_list_number)
325
+ logger.info(f"체인지 리스트 {change_list_number} 제출 성공.")
326
+ return True
327
+ except P4Exception as e:
328
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 제출")
329
+ if any("nothing to submit" in err.lower() for err in self.p4.errors):
330
+ logger.warning(f"체인지 리스트 {change_list_number}에 제출할 파일이 없습니다.")
516
331
  return False
517
332
 
518
- if inDescription:
519
- # 체인지 리스트 스펙 가져오기
520
- change_spec = self._run_command(['change', '-o', inChangelist])
521
-
522
- # 설명 수정
523
- modified_spec = []
524
- description_lines_skipped = False
525
- for line in change_spec.splitlines():
526
- if line.startswith('Description:'):
527
- modified_spec.append(line)
528
- # Add the new description, handling potential multi-line descriptions correctly
529
- for desc_line in inDescription.splitlines():
530
- modified_spec.append(f"\t{desc_line}")
531
- description_lines_skipped = True # Start skipping old description lines
532
- continue
533
-
534
- # Skip old description lines (indented lines after Description:)
535
- if description_lines_skipped:
536
- if line.startswith('\t') or line.strip() == '':
537
- continue # Skip indented lines or empty lines within the old description
538
- else:
539
- description_lines_skipped = False # Reached the next field
540
-
541
- if not description_lines_skipped:
542
- modified_spec.append(line)
543
-
544
- print(modified_spec)
333
+ def revert_change_list(self, change_list_number: int) -> bool:
334
+ """체인지 리스트를 되돌리고 삭제합니다.
545
335
 
546
- # 수정된 스펙 적용
547
- spec_update = subprocess.run(['p4', 'change', '-i'],
548
- input='\n'.join(modified_spec),
549
- capture_output=True,
550
- text=True,
551
- encoding="utf-8")
336
+ 체인지 리스트 모든 파일을 되돌린 후 빈 체인지 리스트를 삭제합니다.
552
337
 
553
- if spec_update.returncode != 0:
554
- print(f"Error updating changelist spec for {inChangelist}: {spec_update.stderr}")
555
- return False
338
+ Args:
339
+ change_list_number (int): 되돌릴 체인지 리스트 번호
556
340
 
557
- self._run_command(['submit', '-c', inChangelist])
558
- self.delete_empty_changelists()
559
- return True
560
-
561
- def get_changelist_files(self, inChangelist, inWorkSpace=None):
562
- """
563
- 체인지 리스트에 포함된 파일 목록을 가져옵니다.
564
-
565
- Parameters:
566
- inChangelist (str): 체인지 리스트 ID
567
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
568
-
569
341
  Returns:
570
- list: 파일 정보 딕셔너리의 리스트
342
+ bool: 되돌리기 삭제 성공 시 True, 실패 시 False
571
343
  """
572
- if inWorkSpace == None:
573
- if self.workspace == None:
574
- print(f"워크스페이스가 없습니다.")
575
- return []
576
- else:
577
- if not self.set_workspace(inWorkSpace):
578
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
579
- return []
344
+ if not self._is_connected():
345
+ return False
346
+ logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 및 삭제 시도...")
347
+ try:
348
+ # 체인지 리스트의 모든 파일 되돌리기
349
+ self.p4.run_revert("-c", change_list_number, "//...")
350
+ logger.info(f"체인지 리스트 {change_list_number} 전체 되돌리기 성공.")
580
351
 
581
- opened_result = self._run_command(['opened', '-c', inChangelist])
582
- files = []
583
-
584
- for line in opened_result.splitlines():
585
- if '#' in line:
586
- file_path = line.split('#')[0].strip()
587
- action = line.split('for ')[1].split(' ')[0] if 'for ' in line else ''
588
-
589
- files.append(file_path)
352
+ # 체인지 리스트 삭제
353
+ try:
354
+ self.p4.run_change("-d", change_list_number)
355
+ logger.info(f"체인지 리스트 {change_list_number} 삭제 완료.")
356
+ except P4Exception as e_delete:
357
+ self._handle_p4_exception(e_delete, f"체인지 리스트 {change_list_number} 삭제")
358
+ logger.warning(f"파일 되돌리기는 성공했으나 체인지 리스트 {change_list_number} 삭제에 실패했습니다.")
359
+ return False
590
360
 
591
- return files
361
+ return True
362
+ except P4Exception as e:
363
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 전체 되돌리기")
364
+ return False
592
365
 
593
- def delete_changelist(self, inChangelist, inWorkSpace=None):
366
+ def delete_empty_change_list(self, change_list_number: int) -> bool:
367
+ """빈 체인지 리스트를 삭제합니다.
368
+
369
+ Args:
370
+ change_list_number (int): 삭제할 체인지 리스트 번호
371
+
372
+ Returns:
373
+ bool: 삭제 성공 시 True, 실패 시 False
594
374
  """
595
- 체인지 리스트를 삭제합니다.
375
+ if not self._is_connected():
376
+ return False
596
377
 
597
- Parameters:
598
- inChangelist (str): 삭제할 체인지 리스트 ID
599
- inWorkspace (str, optional): 작업할 워크스페이스 이름
378
+ logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...")
379
+ try:
380
+ # 체인지 리스트 정보 가져오기
381
+ change_spec = self.p4.fetch_change(change_list_number)
600
382
 
601
- Returns:
602
- bool: 성공 여부
603
- """
604
- if inWorkSpace == None:
605
- if self.workspace == None:
606
- print(f"워크스페이스가 없습니다.")
607
- return False
608
- else:
609
- if not self.set_workspace(inWorkSpace):
610
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
383
+ # 파일이 있는지 확인
384
+ if change_spec and change_spec.get('Files') and len(change_spec['Files']) > 0:
385
+ logger.warning(f"체인지 리스트 {change_list_number}에 파일이 {len(change_spec['Files'])}개 있어 삭제할 수 없습니다.")
611
386
  return False
612
387
 
613
- if not inChangelist:
614
- print("Error: Changelist ID must be provided for delete operation.")
388
+ # 체인지 리스트 삭제
389
+ self.p4.run_change("-d", change_list_number)
390
+ logger.info(f"빈 체인지 리스트 {change_list_number} 삭제 완료.")
391
+ return True
392
+ except P4Exception as e:
393
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number} 삭제")
615
394
  return False
616
-
617
- # 체인지 리스트의 파일 목록을 확인합니다
618
- files = self.get_changelist_files(inChangelist)
619
- if files:
620
- print(f"Error: Changelist {inChangelist} is not empty. It contains {len(files)} files.")
395
+
396
+ def revert_files(self, change_list_number: int, file_paths: list) -> bool:
397
+ """체인지 리스트 내의 특정 파일들을 되돌립니다.
398
+
399
+ Args:
400
+ change_list_number (int): 체인지 리스트 번호
401
+ file_paths (list): 되돌릴 파일 경로 리스트
402
+
403
+ Returns:
404
+ bool: 되돌리기 성공 시 True, 실패 시 False
405
+ """
406
+ if not self._is_connected():
621
407
  return False
408
+ if not file_paths:
409
+ logger.warning("되돌릴 파일 목록이 비어있습니다.")
410
+ return True
622
411
 
623
- # 체인지 리스트를 삭제합니다
624
- delete_result = self._run_command(['change', '-d', inChangelist])
625
-
626
- if 'deleted' in delete_result:
412
+ logger.info(f"체인지 리스트 {change_list_number}에서 {len(file_paths)}개 파일 되돌리기 시도...")
413
+ try:
414
+ for file_path in file_paths:
415
+ self.p4.run_revert("-c", change_list_number, file_path)
416
+ logger.info(f"파일 '{file_path}'를 체인지 리스트 {change_list_number}에서 되돌리기 성공.")
627
417
  return True
628
- else:
418
+ except P4Exception as e:
419
+ self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기")
629
420
  return False
630
421
 
631
- def delete_empty_changelists(self, inWorkSpace=None):
632
- """
633
- 빈 체인지 리스트를 삭제합니다.
634
-
635
- Parameters:
636
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
637
-
638
- Returns:
639
- bool: 성공 여부
640
- """
641
- if inWorkSpace == None:
642
- if self.workspace == None:
643
- print(f"워크스페이스가 없습니다.")
644
- return False
645
- else:
646
- if not self.set_workspace(inWorkSpace):
647
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
648
- return False
649
-
650
- # 모든 체인지 리스트 가져오기
651
- changes = self.get_changelists()
652
-
653
- for change in changes:
654
- if len(self.get_changelist_files(change['id'])) == 0:
655
- self.delete_changelist(change['id'])
656
-
657
- return True
422
+ def check_update_required(self, file_paths: list) -> bool:
423
+ """파일이나 폴더의 업데이트 필요 여부를 확인합니다.
424
+
425
+ Args:
426
+ file_paths (list): 확인할 파일 또는 폴더 경로 리스트.
427
+ 폴더 경로는 자동으로 재귀적으로 처리됩니다.
658
428
 
659
- def upload_files(self, inFiles, inDescription=None, inWorkSpace=None):
660
- """
661
- 지정한 파일들을 Perforce에 Submit 합니다.
662
-
663
- 만약 파일들이 Depot에 존재하지 않으면 Add, 존재하면 Chekcout을 수행합니다.
664
-
665
- Parameters:
666
- inFiles (list): 업로드할 파일 경로 리스트
667
- inWorkSpace (str, optional): 작업할 워크스페이스 이름
668
-
669
429
  Returns:
670
- bool: 성공 여부
430
+ bool: 업데이트가 필요한 파일이 있으면 True, 없으면 False
671
431
  """
672
- if inWorkSpace == None:
673
- if self.workspace == None:
674
- print(f"워크스페이스가 없습니다.")
675
- return False
676
- else:
677
- if not self.set_workspace(inWorkSpace):
678
- print(f"워크스페이스 '{inWorkSpace}'는 로컬 워크스페이스 목록에 없습니다.")
679
- return False
680
-
681
- if not inFiles:
682
- print("업로드할 파일이 지정되지 않았습니다.")
432
+ if not self._is_connected():
683
433
  return False
684
-
685
- # 문자열로 단일 경로가 주어진 경우 리스트로 변환
686
- if isinstance(inFiles, str):
687
- inFiles = [inFiles]
688
-
689
- # 새 체인지리스트 생성
690
- description = inDescription if inDescription else f"Auto-upload for {len(inFiles)} files"
691
- new_changelist_info = self.create_new_changelist(inDescription=description)
692
- if not new_changelist_info:
693
- print("Failed to create a new changelist for file upload.")
434
+ if not file_paths:
435
+ logger.debug("업데이트 필요 여부 확인할 파일/폴더 목록이 비어있습니다.")
694
436
  return False
695
-
696
- target_changelist = new_changelist_info['id']
697
437
 
698
- # 파일들이 이미 디포에 있는지 확인
699
- files_to_add = []
700
- files_to_edit = []
438
+ # 폴더 경로에 재귀적 와일드카드 패턴을 추가
439
+ processed_paths = []
440
+ for path in file_paths:
441
+ if os.path.isdir(path):
442
+ # 폴더 경로에 '...'(재귀) 패턴을 추가
443
+ processed_paths.append(os.path.join(path, '...'))
444
+ logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
445
+ else:
446
+ processed_paths.append(path)
701
447
 
702
- for file_path in inFiles:
703
- # 파일 상태 확인 (디포에 있는지 여부)
704
- fstat_result = self._run_command(['fstat', file_path])
448
+ logger.debug(f"파일/폴더 업데이트 필요 여부 확인 중 (항목 {len(processed_paths)}개): {processed_paths}")
449
+ try:
450
+ sync_preview_results = self.p4.run_sync("-n", processed_paths)
451
+ needs_update = False
452
+ for result in sync_preview_results:
453
+ if isinstance(result, dict):
454
+ if 'up-to-date' not in result.get('how', '') and \
455
+ 'no such file(s)' not in result.get('depotFile', ''):
456
+ if result.get('how') and 'syncing' in result.get('how'):
457
+ needs_update = True
458
+ logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요: {result.get('how')}")
459
+ break
460
+ elif result.get('action') and result.get('action') not in ['checked', 'exists']:
461
+ needs_update = True
462
+ logger.info(f"파일 '{result.get('clientFile', result.get('depotFile'))}' 업데이트 필요 (action: {result.get('action')})")
463
+ break
464
+ elif isinstance(result, str):
465
+ if "up-to-date" not in result and "no such file(s)" not in result:
466
+ needs_update = True
467
+ logger.info(f"파일 업데이트 필요 (문자열 결과): {result}")
468
+ break
705
469
 
706
- if 'no such file' in fstat_result.lower() or not fstat_result:
707
- # 디포에 없는 파일 - 추가 대상
708
- files_to_add.append(file_path)
470
+ if needs_update:
471
+ logger.info(f"지정된 파일/폴더 업데이트가 필요한 파일이 있습니다.")
709
472
  else:
710
- # 디포에 있는 파일 - 체크아웃 대상
711
- files_to_edit.append(file_path)
712
-
713
- # 파일 추가 (있는 경우)
714
- if files_to_add:
715
- add_command = ['add', '-c', target_changelist]
716
- add_command.extend(files_to_add)
717
- self._run_command(add_command)
718
-
719
- # 파일 체크아웃 (있는 경우)
720
- if files_to_edit:
721
- edit_command = ['edit', '-c', target_changelist]
722
- edit_command.extend(files_to_edit)
723
- self._run_command(edit_command)
473
+ logger.info(f"지정된 모든 파일/폴더가 최신 상태입니다.")
474
+ return needs_update
475
+ except P4Exception as e:
476
+ self._handle_p4_exception(e, f"파일/폴더 업데이트 필요 여부 확인 ({processed_paths})")
477
+ return False
478
+
479
+ def sync_files(self, file_paths: list) -> bool:
480
+ """파일이나 폴더를 동기화합니다.
481
+
482
+ Args:
483
+ file_paths (list): 동기화할 파일 또는 폴더 경로 리스트.
484
+ 폴더 경로는 자동으로 재귀적으로 처리됩니다.
485
+
486
+ Returns:
487
+ bool: 동기화 성공 시 True, 실패 시 False
488
+ """
489
+ if not self._is_connected():
490
+ return False
491
+ if not file_paths:
492
+ logger.debug("싱크할 파일/폴더 목록이 비어있습니다.")
493
+ return True
724
494
 
725
- # 체인지리스트에 파일이 제대로 추가되었는지 확인
726
- files_in_changelist = self.get_changelist_files(target_changelist)
495
+ # 폴더 경로에 재귀적 와일드카드 패턴을 추가
496
+ processed_paths = []
497
+ for path in file_paths:
498
+ if os.path.isdir(path):
499
+ # 폴더 경로에 '...'(재귀) 패턴을 추가
500
+ processed_paths.append(os.path.join(path, '...'))
501
+ logger.debug(f"폴더 경로를 재귀 패턴으로 변환: {path} -> {os.path.join(path, '...')}")
502
+ else:
503
+ processed_paths.append(path)
727
504
 
728
- if not files_in_changelist:
729
- # 파일 추가에 실패한 경우 빈 체인지리스트 삭제
730
- self.delete_changelist(target_changelist)
731
- print("파일을 체인지리스트에 추가하는 데 실패했습니다.")
505
+ logger.info(f"파일/폴더 싱크 시도 (항목 {len(processed_paths)}개): {processed_paths}")
506
+ try:
507
+ self.p4.run_sync(processed_paths)
508
+ logger.info(f"파일/폴더 싱크 완료: {processed_paths}")
509
+ return True
510
+ except P4Exception as e:
511
+ self._handle_p4_exception(e, f"파일/폴더 싱크 ({processed_paths})")
732
512
  return False
733
-
734
- print(f"파일 {len(files_in_changelist)}개가 체인지리스트 {target_changelist}에 추가되었습니다.")
735
- return True
513
+
514
+ def disconnect(self):
515
+ """Perforce 서버와의 연결을 해제합니다."""
516
+ if self.connected:
517
+ try:
518
+ self.p4.disconnect()
519
+ self.connected = False
520
+ logger.info("Perforce 서버 연결 해제 완료.")
521
+ except P4Exception as e:
522
+ self._handle_p4_exception(e, "Perforce 서버 연결 해제")
523
+ else:
524
+ logger.debug("Perforce 서버에 이미 연결되지 않은 상태입니다.")
525
+
526
+ def __del__(self):
527
+ """객체가 소멸될 때 자동으로 연결을 해제합니다."""
528
+ self.disconnect()