git.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. # Copyright (c) Alibaba, Inc. and its affiliates.
  2. import os
  3. import subprocess
  4. from typing import List, Optional
  5. from modelscope.utils.logger import get_logger
  6. from ..utils.constant import MASTER_MODEL_BRANCH
  7. from .errors import GitError
  8. logger = get_logger()
  9. class Singleton(type):
  10. _instances = {}
  11. def __call__(cls, *args, **kwargs):
  12. if cls not in cls._instances:
  13. cls._instances[cls] = super(Singleton,
  14. cls).__call__(*args, **kwargs)
  15. return cls._instances[cls]
  16. class GitCommandWrapper(metaclass=Singleton):
  17. """Some git operation wrapper
  18. """
  19. default_git_path = 'git' # The default git command line
  20. def __init__(self, path: str = None):
  21. self.git_path = path or self.default_git_path
  22. def _run_git_command(self, *args) -> subprocess.CompletedProcess:
  23. """Run git command, if command return 0, return subprocess.response
  24. otherwise raise GitError, message is stdout and stderr.
  25. Args:
  26. args: List of command args.
  27. Raises:
  28. GitError: Exception with stdout and stderr.
  29. Returns:
  30. subprocess.CompletedProcess: the command response
  31. """
  32. logger.debug(' '.join(args))
  33. git_env = os.environ.copy()
  34. git_env['GIT_TERMINAL_PROMPT'] = '0'
  35. command = [self.git_path, *args]
  36. command = [item for item in command if item]
  37. response = subprocess.run(
  38. command,
  39. stdout=subprocess.PIPE,
  40. stderr=subprocess.PIPE,
  41. env=git_env,
  42. ) # compatible for python3.6
  43. try:
  44. response.check_returncode()
  45. return response
  46. except subprocess.CalledProcessError as error:
  47. std_out = response.stdout.decode('utf8')
  48. std_err = error.stderr.decode('utf8')
  49. if 'nothing to commit' in std_out:
  50. logger.info(
  51. 'Nothing to commit, your local repo is upto date with remote'
  52. )
  53. return response
  54. else:
  55. logger.error(
  56. 'Running git command: %s failed \n stdout: %s \n stderr: %s'
  57. % (command, std_out, std_err))
  58. raise GitError(std_err)
  59. def config_auth_token(self, repo_dir, auth_token):
  60. url = self.get_repo_remote_url(repo_dir)
  61. if '//oauth2' not in url:
  62. auth_url = self._add_token(auth_token, url)
  63. cmd_args = '-C %s remote set-url origin %s' % (repo_dir, auth_url)
  64. cmd_args = cmd_args.split(' ')
  65. rsp = self._run_git_command(*cmd_args)
  66. logger.debug(rsp.stdout.decode('utf8'))
  67. def _add_token(self, token: str, url: str):
  68. if token:
  69. if '//oauth2' not in url:
  70. url = url.replace('//', '//oauth2:%s@' % token)
  71. return url
  72. def remove_token_from_url(self, url: str):
  73. if url and '//oauth2' in url:
  74. start_index = url.find('oauth2')
  75. end_index = url.find('@')
  76. url = url[:start_index] + url[end_index + 1:]
  77. return url
  78. def is_lfs_installed(self):
  79. cmd = ['lfs', 'env']
  80. try:
  81. self._run_git_command(*cmd)
  82. return True
  83. except GitError:
  84. return False
  85. def git_lfs_install(self, repo_dir):
  86. cmd = ['-C', repo_dir, 'lfs', 'install']
  87. try:
  88. self._run_git_command(*cmd)
  89. return True
  90. except GitError:
  91. return False
  92. def clone(self,
  93. repo_base_dir: str,
  94. token: str,
  95. url: str,
  96. repo_name: str,
  97. branch: Optional[str] = None):
  98. """ git clone command wrapper.
  99. For public project, token can None, private repo, there must token.
  100. Args:
  101. repo_base_dir (str): The local base dir, the repository will be clone to local_dir/repo_name
  102. token (str): The git token, must be provided for private project.
  103. url (str): The remote url
  104. repo_name (str): The local repository path name.
  105. branch (str, optional): _description_. Defaults to None.
  106. Returns:
  107. The popen response.
  108. """
  109. url = self._add_token(token, url)
  110. if branch:
  111. clone_args = '-C %s clone %s %s --branch %s' % (repo_base_dir, url,
  112. repo_name, branch)
  113. else:
  114. clone_args = '-C %s clone %s' % (repo_base_dir, url)
  115. logger.debug(clone_args)
  116. clone_args = clone_args.split(' ')
  117. response = self._run_git_command(*clone_args)
  118. logger.debug(response.stdout.decode('utf8'))
  119. return response
  120. def add_user_info(self, repo_base_dir, repo_name):
  121. from modelscope.hub.api import ModelScopeConfig
  122. user_name, user_email = ModelScopeConfig.get_user_info()
  123. if user_name and user_email:
  124. # config user.name and user.email if exist
  125. config_user_name_args = '-C %s/%s config user.name %s' % (
  126. repo_base_dir, repo_name, user_name)
  127. response = self._run_git_command(*config_user_name_args.split(' '))
  128. logger.debug(response.stdout.decode('utf8'))
  129. config_user_email_args = '-C %s/%s config user.email %s' % (
  130. repo_base_dir, repo_name, user_email)
  131. response = self._run_git_command(
  132. *config_user_email_args.split(' '))
  133. logger.debug(response.stdout.decode('utf8'))
  134. def add(self,
  135. repo_dir: str,
  136. files: List[str] = list(),
  137. all_files: bool = False):
  138. if all_files:
  139. add_args = '-C %s add -A' % repo_dir
  140. elif len(files) > 0:
  141. files_str = ' '.join(files)
  142. add_args = '-C %s add %s' % (repo_dir, files_str)
  143. add_args = add_args.split(' ')
  144. rsp = self._run_git_command(*add_args)
  145. logger.debug(rsp.stdout.decode('utf8'))
  146. return rsp
  147. def commit(self, repo_dir: str, message: str):
  148. """Run git commit command
  149. Args:
  150. repo_dir (str): the repository directory.
  151. message (str): commit message.
  152. Returns:
  153. The command popen response.
  154. """
  155. commit_args = ['-C', '%s' % repo_dir, 'commit', '-m', "'%s'" % message]
  156. rsp = self._run_git_command(*commit_args)
  157. logger.info(rsp.stdout.decode('utf8'))
  158. return rsp
  159. def checkout(self, repo_dir: str, revision: str):
  160. cmds = ['-C', '%s' % repo_dir, 'checkout', '%s' % revision]
  161. return self._run_git_command(*cmds)
  162. def new_branch(self, repo_dir: str, revision: str):
  163. cmds = ['-C', '%s' % repo_dir, 'checkout', '-b', revision]
  164. return self._run_git_command(*cmds)
  165. def get_remote_branches(self, repo_dir: str):
  166. cmds = ['-C', '%s' % repo_dir, 'branch', '-r']
  167. rsp = self._run_git_command(*cmds)
  168. info = [
  169. line.strip()
  170. for line in rsp.stdout.decode('utf8').strip().split(os.linesep)
  171. ]
  172. if len(info) == 1:
  173. return ['/'.join(info[0].split('/')[1:])]
  174. else:
  175. return ['/'.join(line.split('/')[1:]) for line in info[1:]]
  176. def pull(self,
  177. repo_dir: str,
  178. remote: str = 'origin',
  179. branch: str = 'master'):
  180. cmds = ['-C', repo_dir, 'pull', remote, branch]
  181. return self._run_git_command(*cmds)
  182. def push(self,
  183. repo_dir: str,
  184. token: str,
  185. url: str,
  186. local_branch: str,
  187. remote_branch: str,
  188. force: bool = False):
  189. url = self._add_token(token, url)
  190. push_args = '-C %s push %s %s:%s' % (repo_dir, url, local_branch,
  191. remote_branch)
  192. if force:
  193. push_args += ' -f'
  194. push_args = push_args.split(' ')
  195. rsp = self._run_git_command(*push_args)
  196. logger.debug(rsp.stdout.decode('utf8'))
  197. return rsp
  198. def get_repo_remote_url(self, repo_dir: str):
  199. cmd_args = '-C %s config --get remote.origin.url' % repo_dir
  200. cmd_args = cmd_args.split(' ')
  201. rsp = self._run_git_command(*cmd_args)
  202. url = rsp.stdout.decode('utf8')
  203. return url.strip()
  204. def list_lfs_files(self, repo_dir: str):
  205. cmd_args = '-C %s lfs ls-files' % repo_dir
  206. cmd_args = cmd_args.split(' ')
  207. rsp = self._run_git_command(*cmd_args)
  208. out = rsp.stdout.decode('utf8').strip()
  209. files = []
  210. for line in out.split(os.linesep):
  211. files.append(line.split(' ')[-1])
  212. return files
  213. def tag(self,
  214. repo_dir: str,
  215. tag_name: str,
  216. message: str,
  217. ref: str = MASTER_MODEL_BRANCH):
  218. cmd_args = [
  219. '-C', repo_dir, 'tag', tag_name, '-m',
  220. '"%s"' % message, ref
  221. ]
  222. rsp = self._run_git_command(*cmd_args)
  223. logger.debug(rsp.stdout.decode('utf8'))
  224. return rsp
  225. def push_tag(self, repo_dir: str, tag_name):
  226. cmd_args = ['-C', repo_dir, 'push', 'origin', tag_name]
  227. rsp = self._run_git_command(*cmd_args)
  228. logger.debug(rsp.stdout.decode('utf8'))
  229. return rsp