2929from django .utils .encoding import force_str
3030
3131from paasng .platform .sourcectl .exceptions import RequestTimeOutError
32+ from paasng .utils .validators import validate_repo_url
3233
3334logger = logging .getLogger (__name__ )
3435
@@ -37,6 +38,10 @@ class GitCommandExecutionError(Exception):
3738 """执行 git 命令时发生错误"""
3839
3940
41+ class GitURLValidationError (GitCommandExecutionError ):
42+ """Git 仓库 URL 安全校验失败"""
43+
44+
4045@dataclass
4146class Ref :
4247 name : str
@@ -50,22 +55,36 @@ def __str__(self):
5055
5156
5257class GitCommand :
58+ """Git 命令封装。
59+
60+ :param args: Git 子命令选项参数,位于 ``--`` 之前。
61+ :param end_of_options_args: ``--`` 之后的参数(URL、路径、分支名等),
62+ 这些参数不会被 git 解释为选项,即使它们以 ``-`` 开头。
63+ 构建命令时会自动在前面插入 ``--`` 分隔符。
64+ """
65+
5366 def __init__ (
5467 self ,
5568 git_filepath : str ,
5669 command : str ,
57- args : Optional [List [str ]] = None ,
70+ args : List [str ] | None = None ,
71+ end_of_options_args : List [str ] | None = None ,
5872 cwd : str = "" ,
5973 envs : Optional [Dict ] = None ,
6074 ):
6175 self .git_filepath = git_filepath
6276 self .command = command
6377 self .args = args or []
78+ self .end_of_options_args = end_of_options_args or []
6479 self .cwd = cwd
6580 self .envs = envs or {}
6681
6782 def to_cmd (self , obscure : bool = False ) -> List [str ]:
68- return [self .git_filepath , self .command , * self .args ]
83+ cmd = [self .git_filepath , self .command , * self .args ]
84+ if self .end_of_options_args :
85+ cmd .append ("--" )
86+ cmd .extend (self .end_of_options_args )
87+ return cmd
6988
7089 def get_sensitive_texts (self ) -> List [str ]:
7190 """Get sensitive texts in the current command, the texts must not be displayed
@@ -93,13 +112,14 @@ def __init__(
93112 SYNOPSIS
94113 git clone [--bare] [--] <repository> [<target_directory>]
95114 """
96- super ().__init__ (git_filepath , "clone" , args , cwd , envs )
115+ super ().__init__ (git_filepath = git_filepath , command = "clone" , args = args , cwd = cwd , envs = envs )
97116 self .repository = repository
98117 self .target_directory = target_directory
99118
100119 def to_cmd (self , obscure : bool = False ) -> List [str ]:
101120 """"""
102121 cmd = super ().to_cmd (obscure = obscure )
122+ cmd .append ("--" )
103123 cmd .extend ([str (self .repository .obscure ()) if obscure else str (self .repository ), self .target_directory ])
104124 return cmd
105125
@@ -128,22 +148,26 @@ class GitClient:
128148
129149 def checkout (self , path : Path , target : str ) -> str :
130150 """切换分支或tag"""
131- command = GitCommand (git_filepath = self ._git_filepath , command = "checkout" , args = [target ], cwd = str (path ))
151+ command = GitCommand (
152+ git_filepath = self ._git_filepath , command = "checkout" , end_of_options_args = [target ], cwd = str (path )
153+ )
132154 return self .run (command )
133155
134156 def clone (
135157 self ,
136158 url : Union [str , MutableURL ],
137159 path : Path ,
138- envs : Optional [ dict ] = None ,
139- depth : Optional [ int ] = None ,
140- branch : Optional [ str ] = None ,
160+ envs : Dict | None = None ,
161+ depth : int | None = None ,
162+ branch : str | None = None ,
141163 ) -> str :
142164 """克隆仓库
143165
144166 :param url: 仓库地址
145167 :param path: 存储路径
146168 :param envs: 环境变量
169+ :param depth: 克隆深度
170+ :param branch: 克隆分支
147171 :return: 返回 clone 结果
148172 """
149173 args = []
@@ -182,8 +206,7 @@ def clone_no_blob(self, url: Union[str, MutableURL], path: Path) -> str:
182206 def list_remote (self , url : Union [str , MutableURL ]) -> List [Tuple [str , str ]]:
183207 """通过 ls-remote 命令,直接获取远端仓库的 branch 与 tag 等信息。该命令不依赖本地文件系统。
184208
185- :param path: 项目路径
186- :param remote: 远端名称,默认为 origin
209+ :param url: 仓库地址
187210 :return: 命令执行结果,包含 (commit_id, ref) 的列表
188211 """
189212 results = []
@@ -206,12 +229,20 @@ def list_remote(self, url: Union[str, MutableURL]) -> List[Tuple[str, str]]:
206229 def list_remote_raw (self , url : Union [str , MutableURL ]) -> str :
207230 """通过 ls-remote 命令,直接获取远端仓库的 branch 与 tag 等信息。返回命令原始执行结果。
208231
209- :param path: 项目路径
210- :param remote: 远端名称,默认为 origin
232+ :param url: 仓库地址
211233 :return: 命令原始执行结果
212234 """
235+ # 在执行 Git 操作前校验 URL 安全性,防止历史脏数据绕过
236+ url_str = str (url )
237+ try :
238+ validate_repo_url (url_str )
239+ except ValueError as e :
240+ raise GitURLValidationError (str (e ))
241+
213242 # The "cwd" is pointless for running this command, always use current dir.
214- command = GitCommand (git_filepath = self ._git_filepath , command = "ls-remote" , args = [str (url )], cwd = os .getcwd ())
243+ command = GitCommand (
244+ git_filepath = self ._git_filepath , command = "ls-remote" , end_of_options_args = [url_str ], cwd = os .getcwd ()
245+ )
215246 return self .run (command )
216247
217248 def list_refs (self , path : Path ) -> Generator [Ref , None , None ]:
0 commit comments