pyjallib 0.1.8__py3-none-any.whl → 0.1.9__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/__init__.py +1 -1
- pyjallib/max/autoClavicle.py +85 -42
- pyjallib/max/bone.py +14 -7
- pyjallib/max/constraint.py +29 -38
- pyjallib/max/groinBone.py +51 -20
- pyjallib/max/header.py +4 -6
- pyjallib/max/hip.py +366 -0
- pyjallib/max/twistBone.py +1 -1
- pyjallib/max/volumePreserveBone.py +64 -26
- pyjallib/p4module.py +488 -0
- pyjallib/perforce.py +446 -653
- {pyjallib-0.1.8.dist-info → pyjallib-0.1.9.dist-info}/METADATA +1 -1
- {pyjallib-0.1.8.dist-info → pyjallib-0.1.9.dist-info}/RECORD +14 -12
- {pyjallib-0.1.8.dist-info → pyjallib-0.1.9.dist-info}/WHEEL +0 -0
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
|
3
|
-
import socket
|
15
|
+
from pathlib import Path
|
4
16
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
150
|
-
|
151
|
-
Raises:
|
152
|
-
ValueError: 지정된 워크스페이스가 로컬 워크스페이스 목록에 없을 경우
|
41
|
+
bool: 연결되어 있으면 True, 아니면 False
|
153
42
|
"""
|
154
|
-
|
155
|
-
|
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
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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: 생성된 체인지 리스트
|
130
|
+
dict: 생성된 체인지 리스트 정보. 실패 시 빈 딕셔너리
|
270
131
|
"""
|
271
|
-
if
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
156
|
+
dict: 체인지 리스트 정보. 실패 시 빈 딕셔너리
|
335
157
|
"""
|
336
|
-
if
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
if
|
342
|
-
|
343
|
-
return
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
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
|
-
|
180
|
+
dict: 체인지 리스트 정보 (일치하는 첫 번째 체인지 리스트)
|
380
181
|
"""
|
381
|
-
if
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
405
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
451
|
-
|
278
|
+
Args:
|
279
|
+
file_path (str): 체크아웃할 파일 경로
|
280
|
+
change_list_number (int): 체인지 리스트 번호
|
452
281
|
|
453
|
-
|
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
|
-
|
285
|
+
return self._file_op("edit", file_path, change_list_number, "체크아웃")
|
463
286
|
|
464
|
-
|
465
|
-
|
466
|
-
|
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
|
-
|
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
|
-
|
485
|
-
|
299
|
+
def delete_file(self, file_path: str, change_list_number: int) -> bool:
|
300
|
+
"""파일을 삭제합니다.
|
486
301
|
|
487
|
-
|
488
|
-
|
489
|
-
|
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
|
-
|
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
|
-
|
499
|
-
|
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
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
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
|
-
|
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
|
-
|
554
|
-
|
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
|
-
|
342
|
+
bool: 되돌리기 및 삭제 성공 시 True, 실패 시 False
|
571
343
|
"""
|
572
|
-
if
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
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
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
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
|
-
|
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
|
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
|
-
|
598
|
-
|
599
|
-
|
378
|
+
logger.info(f"체인지 리스트 {change_list_number} 삭제 시도 중...")
|
379
|
+
try:
|
380
|
+
# 체인지 리스트 정보 가져오기
|
381
|
+
change_spec = self.p4.fetch_change(change_list_number)
|
600
382
|
|
601
|
-
|
602
|
-
|
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
|
-
|
614
|
-
|
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
|
-
|
619
|
-
|
620
|
-
|
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
|
-
|
625
|
-
|
626
|
-
|
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
|
-
|
418
|
+
except P4Exception as e:
|
419
|
+
self._handle_p4_exception(e, f"체인지 리스트 {change_list_number}에서 파일 되돌리기")
|
629
420
|
return False
|
630
421
|
|
631
|
-
def
|
632
|
-
"""
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
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
|
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
|
-
|
700
|
-
|
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
|
-
|
703
|
-
|
704
|
-
|
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
|
707
|
-
|
708
|
-
files_to_add.append(file_path)
|
470
|
+
if needs_update:
|
471
|
+
logger.info(f"지정된 파일/폴더 중 업데이트가 필요한 파일이 있습니다.")
|
709
472
|
else:
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
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
|
-
|
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
|
-
|
729
|
-
|
730
|
-
self.
|
731
|
-
|
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
|
-
|
735
|
-
|
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()
|