P4Python:登录方式、reconcile 坑与从 changelist 生成 git patch
P4Python:登录方式、reconcile 坑与从 changelist 生成 git patch
用 P4Python 写自动化脚本时,绕不开两件事:怎么登录,以及 run_reconcile 返回值里混着的字符串怎么处理。最后附一个我做的 P4V 自定义工具——把一个 changelist 导出成 git patch。
登录方式
用密码登录
from P4 import P4
# from os import environ
p4 = P4()
p4.port = "<P4SERVER>:1666"
p4.user = "<user>"
# 推荐从环境变量读密码,别写死在脚本里
# p4.password = environ.get("p4psd")
p4.password = "<password>"
p4.connect()
用 Ticket 登录
from P4 import P4
p4 = P4()
p4.port = "<P4SERVER>:1666"
p4.user = "<user>"
p4.connect()
p4.run_login("-s", p4.user)
reconcile 的空文件返回值
用 run_reconcile 时有个容易踩的坑:文件列表里的空文件(会被当作 add),reconcile 返回的结果里会多出一条字符串类型的消息,而不是结构化的 dict。也就是说返回结果的长度会大于实际进 pending changelist 的文件数。
遍历时要按类型区分:字符串是空文件提示(不进 pending),dict 才是真正被 reconcile 的文件。
if recon := p4_l.run_reconcile("-c", c_new_cl, "-f", c_update_list):
empty_files = 0
recon_files = []
record_file_list(f"{record_root}\\ReconcileList.txt", recon)
for reconcile_msg in recon:
if type(reconcile_msg) == str:
print(reconcile_msg) # 空文件提示
empty_files += 1
else:
recon_files.append(reconcile_msg['depotFile'])
print(f"Number of Update filelist: {len(recon)}")
print(f"Number of No reconcile filelist: {empty_files}")
print(f"Number of pending filelist: {len(recon_files)}")
空文件返回的是这样一条字符串:
'.../site-packages/scipy/spatial/transform/tests/__init__.py - empty, assuming text'
正常文件返回的是 dict:
{'depotFile': '//depot/.../scipy/special/_precompute/__init__.py',
'clientFile': 'E:\\...\\__init__.py',
'workRev': '1', 'action': 'add', 'type': 'text'}
实测过一次:reconcile 返回结果长度 4378,实际进 pending changelist 的文件数 4284,其中字符串类型(空文件)94 条——4378 = 4284 + 94 正好对上。所以统计文件数时一定要把字符串那部分剔掉。
P4V 自定义工具:从 changelist 生成 git patch
这是个 P4V 右键 changelist 就能用的自定义工具。装法:把脚本放到大家都能访问的共享文件夹,改好 P4CustomTools.xml 和 Register.bat 里的路径,双击 Register.bat 安装;在 P4V 里右键 changelist 就能看到选项,看不到就重启 P4V。
两个限制:
- 只能用于 stream 仓库。
- 选 pending changelist 时,diff 的是 shelve 的修改。
脚本接收 changelist 号和 workspace 根目录两个参数,先用 p4 describe 判断是 pending 还是 submitted(两者用的 describe 选项不同、取 stream 名的方式也不同),再把 ==== //stream/path#rev ==== 这种 Perforce 文件头改写成 diff --git 格式,最后把行尾统一成 LF:
import sys
import re
import os
import subprocess
from P4 import P4, P4Exception
def convert_crlf_to_lf_inplace(file_path):
with open(file_path, 'r', newline=None) as f:
content = f.read()
with open(file_path, 'w', newline='\n') as f:
f.write(content)
def get_real_stream_name(stream_name, p4):
out_streams = p4.run('streams')
stream = stream_name
for dic in out_streams:
if dic['Stream'] == stream_name:
stream_type = dic['Type']
if stream_type == 'virtual':
stream = dic['baseParent']
break
return stream
def get_stream_name(p4, depotFile):
out_streams = p4.run('streams')
stream = ''
for dic in out_streams:
s = dic['Stream']
if depotFile.startswith(s):
stream = s
break
if not stream:
print('Get stream name error.')
sys.exit()
else:
return stream
if __name__ == "__main__":
cl = sys.argv[1]
workspace_root = sys.argv[2]
if cl == "default":
print("Please select a numbered change list.")
sys.exit()
try:
p4 = P4()
p4 = p4.connect()
# 判断点击的是 pending cl 还是 submitted cl
out = p4.run('describe', cl)
status = out[0]['status']
if status == 'pending':
cmd = ['p4', 'describe', '-S', '-du10', cl]
out_client = p4.run('client', '-o')
stream = get_real_stream_name(out_client[0]['Stream'], p4)
elif status == 'submitted':
cmd = ['p4', 'describe', '-du10', cl]
depotFile = out[0]['depotFile'][0]
stream = get_stream_name(p4, depotFile)
else:
print('Unknow error.')
sys.exit()
print(rf"Real stream name: {stream}")
tmp_file = rf'{workspace_root}\{cl}.tmp'
git_patch_file = rf'{workspace_root}\{cl}.patch'
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
output_text = result.stdout
with open(tmp_file, 'w', encoding='utf-8') as f:
f.write(output_text)
# 匹配原始行中的路径和文件名部分
pattern = re.compile(r'^==== (//.+?)#\d+ .+? ====$')
# 替换文件标识行
with open(tmp_file, 'r', encoding='utf-8') as infile:
with open(git_patch_file, 'w', encoding='utf-8') as outfile:
for line in infile:
match = pattern.match(line.strip())
if match:
path = match.group(1)
new_path = path.replace(stream, '')
if new_path == path:
print('The stream name is incorrect.')
sys.exit()
outfile.write(f'diff --git a{new_path} b{new_path}\n')
outfile.write(f'--- a{new_path}\n')
outfile.write(f'+++ b{new_path}')
else:
outfile.write(line)
# 转换行尾为 LF
convert_crlf_to_lf_inplace(git_patch_file)
print(f'Git patch file has been generated to: {git_patch_file}')
# 删除 tmp 文件
if os.path.exists(tmp_file):
os.remove(tmp_file)
except subprocess.CalledProcessError as e:
print(f"The command execution failed, error message:\n{e.stderr}")
except FileNotFoundError:
print("The p4 command was not found. Please ensure that the Perforce client is installed and the command is included in your PATH.")
关键点在那条正则:==== (//.+?)#\d+ .+? ====。把匹配到的 depot 路径去掉 stream 前缀,就得到 git patch 里相对仓库根的路径;如果去掉前缀后路径没变,说明 stream 名取错了,直接退出。