pyjallib 0.1.0__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 +17 -0
- pyjallib/max/__init__.py +46 -0
- pyjallib/max/align.py +112 -0
- pyjallib/max/anim.py +594 -0
- pyjallib/max/bip.py +508 -0
- pyjallib/max/bone.py +910 -0
- pyjallib/max/constraint.py +973 -0
- pyjallib/max/header.py +57 -0
- pyjallib/max/helper.py +433 -0
- pyjallib/max/layer.py +262 -0
- pyjallib/max/link.py +78 -0
- pyjallib/max/macro/jal_macro_align.py +155 -0
- pyjallib/max/macro/jal_macro_bone.py +358 -0
- pyjallib/max/macro/jal_macro_constraint.py +140 -0
- pyjallib/max/macro/jal_macro_helper.py +321 -0
- pyjallib/max/macro/jal_macro_link.py +55 -0
- pyjallib/max/macro/jal_macro_select.py +91 -0
- pyjallib/max/mirror.py +388 -0
- pyjallib/max/name.py +521 -0
- pyjallib/max/select.py +278 -0
- pyjallib/max/skin.py +996 -0
- pyjallib/max/twistBone.py +418 -0
- pyjallib/namePart.py +633 -0
- pyjallib/nameToPath.py +113 -0
- pyjallib/naming.py +1066 -0
- pyjallib/namingConfig.py +844 -0
- pyjallib/perforce.py +735 -0
- pyjallib/reloadModules.py +33 -0
- pyjallib-0.1.0.dist-info/METADATA +28 -0
- pyjallib-0.1.0.dist-info/RECORD +32 -0
- pyjallib-0.1.0.dist-info/WHEEL +5 -0
- pyjallib-0.1.0.dist-info/top_level.txt +1 -0
pyjallib/perforce.py
ADDED
@@ -0,0 +1,735 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
import socket
|
4
|
+
|
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()
|
91
|
+
|
92
|
+
def get_local_hostname(self):
|
93
|
+
"""
|
94
|
+
현재 로컬 머신의 호스트 이름을 반환합니다.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
str: 로컬 머신의 호스트 이름
|
98
|
+
"""
|
99
|
+
# 현재 로컬 머신의 호스트 이름을 반환합니다.
|
100
|
+
return self.localHostName
|
101
|
+
|
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 = []
|
134
|
+
|
135
|
+
for client in all_clients:
|
136
|
+
if client.startswith(self.localHostName):
|
137
|
+
local_clients.append(client)
|
138
|
+
|
139
|
+
return local_clients
|
140
|
+
|
141
|
+
def set_workspace(self, inWorkspace):
|
142
|
+
"""
|
143
|
+
주어진 워크스페이스로 현재 작업 환경을 전환합니다.
|
144
|
+
|
145
|
+
Parameters:
|
146
|
+
inWorkspace (str): 전환할 워크스페이스 이름
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
str: 워크스페이스 정보 문자열
|
150
|
+
|
151
|
+
Raises:
|
152
|
+
ValueError: 지정된 워크스페이스가 로컬 워크스페이스 목록에 없을 경우
|
153
|
+
"""
|
154
|
+
# 주어진 워크스페이스로 전환합니다.
|
155
|
+
localWorkSpaces = self.get_local_workspaces()
|
156
|
+
if inWorkspace not in localWorkSpaces:
|
157
|
+
print(f"워크스페이스 '{inWorkspace}'는 로컬 워크스페이스 목록에 없습니다.")
|
158
|
+
return False
|
159
|
+
|
160
|
+
self.workspace = inWorkspace
|
161
|
+
os.environ['P4CLIENT'] = self.workspace
|
162
|
+
|
163
|
+
return True
|
164
|
+
|
165
|
+
def sync(self, inWorkSpace=None, inPaths=None):
|
166
|
+
"""
|
167
|
+
Perforce 워크스페이스를 최신 버전으로 동기화합니다.
|
168
|
+
|
169
|
+
Parameters:
|
170
|
+
inWorkSpace (str, optional): 동기화할 워크스페이스 이름
|
171
|
+
inPaths (str or list, optional): 동기화할 특정 경로 또는 경로 목록
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
bool: 동기화 성공 여부
|
175
|
+
"""
|
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}'가 로컬에 존재하지 않습니다.")
|
200
|
+
|
201
|
+
if not valid_paths:
|
202
|
+
print("유효한 경로가 없어 동기화를 진행할 수 없습니다.")
|
203
|
+
return False
|
204
|
+
|
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
|
+
|
218
|
+
Returns:
|
219
|
+
list: 체인지 리스트 정보 딕셔너리의 리스트
|
220
|
+
"""
|
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'])
|
235
|
+
|
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): 체인지 리스트를 생성할 워크스페이스 이름
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
dict: 생성된 체인지 리스트 정보 {'id': str, 'description': str} 또는 실패 시 None
|
270
|
+
"""
|
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
|
+
|
333
|
+
Returns:
|
334
|
+
bool: 성공 여부
|
335
|
+
"""
|
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']
|
358
|
+
|
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
|
+
Returns:
|
379
|
+
bool: 성공 여부
|
380
|
+
"""
|
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']
|
403
|
+
|
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):
|
415
|
+
"""
|
416
|
+
지정한 파일들을 Perforce에서 삭제합니다.
|
417
|
+
|
418
|
+
Parameters:
|
419
|
+
inFiles (list): 삭제할 파일 경로 리스트
|
420
|
+
inChangelist (str, optional): 파일 삭제를 추가할 체인지 리스트 ID
|
421
|
+
inWorkSpace (str, optional): 작업할 워크스페이스 이름
|
422
|
+
|
423
|
+
Returns:
|
424
|
+
bool: 성공 여부
|
425
|
+
"""
|
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}'는 로컬 워크스페이스 목록에 없습니다.")
|
433
|
+
return False
|
434
|
+
|
435
|
+
if not inFiles:
|
436
|
+
return False
|
437
|
+
|
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']
|
449
|
+
|
450
|
+
delete_command.extend(['-c', target_changelist])
|
451
|
+
delete_command.extend(inFiles)
|
452
|
+
|
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):
|
461
|
+
"""
|
462
|
+
특정 체인지 리스트의 모든 변경 사항을 되돌립니다 (revert).
|
463
|
+
|
464
|
+
Parameters:
|
465
|
+
inChangelist (str): 되돌릴 체인지 리스트 ID
|
466
|
+
inWorkSpace (str, optional): 작업할 워크스페이스 이름
|
467
|
+
|
468
|
+
Returns:
|
469
|
+
bool: 성공 여부
|
470
|
+
"""
|
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
|
483
|
+
|
484
|
+
# Revert 명령 실행
|
485
|
+
revert_command = ['revert', '-c', inChangelist, '//...'] # Revert all files in the changelist
|
486
|
+
|
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
|
493
|
+
|
494
|
+
def submit_changelist(self, inChangelist, inDescription=None, inWorkSpace=None):
|
495
|
+
"""
|
496
|
+
특정 체인지 리스트를 서버에 제출합니다.
|
497
|
+
|
498
|
+
Parameters:
|
499
|
+
inChangelist (str): 제출할 체인지 리스트 ID
|
500
|
+
inDescription (str, optional): 제출 설명 (없으면 체인지 리스트의 기존 설명 사용)
|
501
|
+
inWorkSpace (str, optional): 작업할 워크스페이스 이름
|
502
|
+
|
503
|
+
Returns:
|
504
|
+
bool: 성공 여부
|
505
|
+
"""
|
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:
|
516
|
+
return False
|
517
|
+
|
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)
|
545
|
+
|
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")
|
552
|
+
|
553
|
+
if spec_update.returncode != 0:
|
554
|
+
print(f"Error updating changelist spec for {inChangelist}: {spec_update.stderr}")
|
555
|
+
return False
|
556
|
+
|
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
|
+
Returns:
|
570
|
+
list: 파일 정보 딕셔너리의 리스트
|
571
|
+
"""
|
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 []
|
580
|
+
|
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)
|
590
|
+
|
591
|
+
return files
|
592
|
+
|
593
|
+
def delete_changelist(self, inChangelist, inWorkSpace=None):
|
594
|
+
"""
|
595
|
+
빈 체인지 리스트를 삭제합니다.
|
596
|
+
|
597
|
+
Parameters:
|
598
|
+
inChangelist (str): 삭제할 체인지 리스트 ID
|
599
|
+
inWorkspace (str, optional): 작업할 워크스페이스 이름
|
600
|
+
|
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}'는 로컬 워크스페이스 목록에 없습니다.")
|
611
|
+
return False
|
612
|
+
|
613
|
+
if not inChangelist:
|
614
|
+
print("Error: Changelist ID must be provided for delete operation.")
|
615
|
+
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.")
|
621
|
+
return False
|
622
|
+
|
623
|
+
# 빈 체인지 리스트를 삭제합니다
|
624
|
+
delete_result = self._run_command(['change', '-d', inChangelist])
|
625
|
+
|
626
|
+
if 'deleted' in delete_result:
|
627
|
+
return True
|
628
|
+
else:
|
629
|
+
return False
|
630
|
+
|
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
|
658
|
+
|
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
|
+
Returns:
|
670
|
+
bool: 성공 여부
|
671
|
+
"""
|
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("업로드할 파일이 지정되지 않았습니다.")
|
683
|
+
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.")
|
694
|
+
return False
|
695
|
+
|
696
|
+
target_changelist = new_changelist_info['id']
|
697
|
+
|
698
|
+
# 파일들이 이미 디포에 있는지 확인
|
699
|
+
files_to_add = []
|
700
|
+
files_to_edit = []
|
701
|
+
|
702
|
+
for file_path in inFiles:
|
703
|
+
# 파일 상태 확인 (디포에 있는지 여부)
|
704
|
+
fstat_result = self._run_command(['fstat', file_path])
|
705
|
+
|
706
|
+
if 'no such file' in fstat_result.lower() or not fstat_result:
|
707
|
+
# 디포에 없는 파일 - 추가 대상
|
708
|
+
files_to_add.append(file_path)
|
709
|
+
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)
|
724
|
+
|
725
|
+
# 체인지리스트에 파일이 제대로 추가되었는지 확인
|
726
|
+
files_in_changelist = self.get_changelist_files(target_changelist)
|
727
|
+
|
728
|
+
if not files_in_changelist:
|
729
|
+
# 파일 추가에 실패한 경우 빈 체인지리스트 삭제
|
730
|
+
self.delete_changelist(target_changelist)
|
731
|
+
print("파일을 체인지리스트에 추가하는 데 실패했습니다.")
|
732
|
+
return False
|
733
|
+
|
734
|
+
print(f"파일 {len(files_in_changelist)}개가 체인지리스트 {target_changelist}에 추가되었습니다.")
|
735
|
+
return True
|