1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li""" 3*9c5db199SXin LiModule with abstraction layers to revision control systems. 4*9c5db199SXin Li 5*9c5db199SXin LiWith this library, autotest developers can handle source code checkouts and 6*9c5db199SXin Liupdates on both client as well as server code. 7*9c5db199SXin Li""" 8*9c5db199SXin Li 9*9c5db199SXin Lifrom __future__ import absolute_import 10*9c5db199SXin Lifrom __future__ import division 11*9c5db199SXin Lifrom __future__ import print_function 12*9c5db199SXin Li 13*9c5db199SXin Liimport os, warnings, logging 14*9c5db199SXin Liimport six 15*9c5db199SXin Li 16*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 17*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 18*9c5db199SXin Lifrom autotest_lib.client.bin import os_dep 19*9c5db199SXin Li 20*9c5db199SXin Li 21*9c5db199SXin Liclass RevisionControlError(Exception): 22*9c5db199SXin Li """Local exception to be raised by code in this file.""" 23*9c5db199SXin Li 24*9c5db199SXin Li 25*9c5db199SXin Liclass GitError(RevisionControlError): 26*9c5db199SXin Li """Exceptions raised for general git errors.""" 27*9c5db199SXin Li 28*9c5db199SXin Li 29*9c5db199SXin Liclass GitCloneError(GitError): 30*9c5db199SXin Li """Exceptions raised for git clone errors.""" 31*9c5db199SXin Li 32*9c5db199SXin Li 33*9c5db199SXin Liclass GitFetchError(GitError): 34*9c5db199SXin Li """Exception raised for git fetch errors.""" 35*9c5db199SXin Li 36*9c5db199SXin Li 37*9c5db199SXin Liclass GitPullError(GitError): 38*9c5db199SXin Li """Exception raised for git pull errors.""" 39*9c5db199SXin Li 40*9c5db199SXin Li 41*9c5db199SXin Liclass GitResetError(GitError): 42*9c5db199SXin Li """Exception raised for git reset errors.""" 43*9c5db199SXin Li 44*9c5db199SXin Li 45*9c5db199SXin Liclass GitCommitError(GitError): 46*9c5db199SXin Li """Exception raised for git commit errors.""" 47*9c5db199SXin Li 48*9c5db199SXin Li 49*9c5db199SXin Liclass GitPushError(GitError): 50*9c5db199SXin Li """Exception raised for git push errors.""" 51*9c5db199SXin Li 52*9c5db199SXin Li 53*9c5db199SXin Liclass GitRepo(object): 54*9c5db199SXin Li """ 55*9c5db199SXin Li This class represents a git repo. 56*9c5db199SXin Li 57*9c5db199SXin Li It is used to pull down a local copy of a git repo, check if the local 58*9c5db199SXin Li repo is up-to-date, if not update. It delegates the install to 59*9c5db199SXin Li implementation classes. 60*9c5db199SXin Li """ 61*9c5db199SXin Li 62*9c5db199SXin Li def __init__(self, repodir, giturl=None, weburl=None, abs_work_tree=None): 63*9c5db199SXin Li """ 64*9c5db199SXin Li Initialized reposotory. 65*9c5db199SXin Li 66*9c5db199SXin Li @param repodir: destination repo directory. 67*9c5db199SXin Li @param giturl: main repo git url. 68*9c5db199SXin Li @param weburl: a web url for the main repo. 69*9c5db199SXin Li @param abs_work_tree: work tree of the git repo. In the 70*9c5db199SXin Li absence of a work tree git manipulations will occur 71*9c5db199SXin Li in the current working directory for non bare repos. 72*9c5db199SXin Li In such repos the -git-dir option should point to 73*9c5db199SXin Li the .git directory and -work-tree should point to 74*9c5db199SXin Li the repos working tree. 75*9c5db199SXin Li Note: a bare reposotory is one which contains all the 76*9c5db199SXin Li working files (the tree) and the other wise hidden files 77*9c5db199SXin Li (.git) in the same directory. This class assumes non-bare 78*9c5db199SXin Li reposotories. 79*9c5db199SXin Li """ 80*9c5db199SXin Li if repodir is None: 81*9c5db199SXin Li raise ValueError('You must provide a path that will hold the' 82*9c5db199SXin Li 'git repository') 83*9c5db199SXin Li self.repodir = utils.sh_escape(repodir) 84*9c5db199SXin Li self._giturl = giturl 85*9c5db199SXin Li if weburl is not None: 86*9c5db199SXin Li warnings.warn("Param weburl: You are no longer required to provide " 87*9c5db199SXin Li "a web URL for your git repos", DeprecationWarning) 88*9c5db199SXin Li 89*9c5db199SXin Li # path to .git dir 90*9c5db199SXin Li self.gitpath = utils.sh_escape(os.path.join(self.repodir,'.git')) 91*9c5db199SXin Li 92*9c5db199SXin Li # Find git base command. If not found, this will throw an exception 93*9c5db199SXin Li self.git_base_cmd = os_dep.command('git') 94*9c5db199SXin Li self.work_tree = abs_work_tree 95*9c5db199SXin Li 96*9c5db199SXin Li # default to same remote path as local 97*9c5db199SXin Li self._build = os.path.dirname(self.repodir) 98*9c5db199SXin Li 99*9c5db199SXin Li 100*9c5db199SXin Li @property 101*9c5db199SXin Li def giturl(self): 102*9c5db199SXin Li """ 103*9c5db199SXin Li A giturl is necessary to perform certain actions (clone, pull, fetch) 104*9c5db199SXin Li but not others (like diff). 105*9c5db199SXin Li """ 106*9c5db199SXin Li if self._giturl is None: 107*9c5db199SXin Li raise ValueError('Unsupported operation -- this object was not' 108*9c5db199SXin Li 'constructed with a git URL.') 109*9c5db199SXin Li return self._giturl 110*9c5db199SXin Li 111*9c5db199SXin Li 112*9c5db199SXin Li def gen_git_cmd_base(self): 113*9c5db199SXin Li """ 114*9c5db199SXin Li The command we use to run git cannot be set. It is reconstructed 115*9c5db199SXin Li on each access from it's component variables. This is it's getter. 116*9c5db199SXin Li """ 117*9c5db199SXin Li # base git command , pointing to gitpath git dir 118*9c5db199SXin Li gitcmdbase = '%s --git-dir=%s' % (self.git_base_cmd, 119*9c5db199SXin Li self.gitpath) 120*9c5db199SXin Li if self.work_tree: 121*9c5db199SXin Li gitcmdbase += ' --work-tree=%s' % self.work_tree 122*9c5db199SXin Li return gitcmdbase 123*9c5db199SXin Li 124*9c5db199SXin Li 125*9c5db199SXin Li def _run(self, command, timeout=None, ignore_status=False): 126*9c5db199SXin Li """ 127*9c5db199SXin Li Auxiliary function to run a command, with proper shell escaping. 128*9c5db199SXin Li 129*9c5db199SXin Li @param timeout: Timeout to run the command. 130*9c5db199SXin Li @param ignore_status: Whether we should supress error.CmdError 131*9c5db199SXin Li exceptions if the command did return exit code !=0 (True), or 132*9c5db199SXin Li not supress them (False). 133*9c5db199SXin Li """ 134*9c5db199SXin Li return utils.run(r'%s' % (utils.sh_escape(command)), 135*9c5db199SXin Li timeout, ignore_status) 136*9c5db199SXin Li 137*9c5db199SXin Li 138*9c5db199SXin Li def gitcmd(self, cmd, ignore_status=False, error_class=None, 139*9c5db199SXin Li error_msg=None): 140*9c5db199SXin Li """ 141*9c5db199SXin Li Wrapper for a git command. 142*9c5db199SXin Li 143*9c5db199SXin Li @param cmd: Git subcommand (ex 'clone'). 144*9c5db199SXin Li @param ignore_status: If True, ignore the CmdError raised by the 145*9c5db199SXin Li underlying command runner. NB: Passing in an error_class 146*9c5db199SXin Li impiles ignore_status=True. 147*9c5db199SXin Li @param error_class: When ignore_status is False, optional error 148*9c5db199SXin Li error class to log and raise in case of errors. Must be a 149*9c5db199SXin Li (sub)type of GitError. 150*9c5db199SXin Li @param error_msg: When passed with error_class, used as a friendly 151*9c5db199SXin Li error message. 152*9c5db199SXin Li """ 153*9c5db199SXin Li # TODO(pprabhu) Get rid of the ignore_status argument. 154*9c5db199SXin Li # Now that we support raising custom errors, we always want to get a 155*9c5db199SXin Li # return code from the command execution, instead of an exception. 156*9c5db199SXin Li ignore_status = ignore_status or error_class is not None 157*9c5db199SXin Li cmd = '%s %s' % (self.gen_git_cmd_base(), cmd) 158*9c5db199SXin Li rv = self._run(cmd, ignore_status=ignore_status) 159*9c5db199SXin Li if rv.exit_status != 0 and error_class is not None: 160*9c5db199SXin Li logging.error('git command failed: %s: %s', 161*9c5db199SXin Li cmd, error_msg if error_msg is not None else '') 162*9c5db199SXin Li logging.error(rv.stderr) 163*9c5db199SXin Li raise error_class(error_msg if error_msg is not None 164*9c5db199SXin Li else rv.stderr) 165*9c5db199SXin Li 166*9c5db199SXin Li return rv 167*9c5db199SXin Li 168*9c5db199SXin Li 169*9c5db199SXin Li def clone(self, remote_branch=None, shallow=False): 170*9c5db199SXin Li """ 171*9c5db199SXin Li Clones a repo using giturl and repodir. 172*9c5db199SXin Li 173*9c5db199SXin Li Since we're cloning the main repo we don't have a work tree yet, 174*9c5db199SXin Li make sure the getter of the gitcmd doesn't think we do by setting 175*9c5db199SXin Li work_tree to None. 176*9c5db199SXin Li 177*9c5db199SXin Li @param remote_branch: Specify the remote branch to clone. None if to 178*9c5db199SXin Li clone main branch. 179*9c5db199SXin Li @param shallow: If True, do a shallow clone. 180*9c5db199SXin Li 181*9c5db199SXin Li @raises GitCloneError: if cloning the main repo fails. 182*9c5db199SXin Li """ 183*9c5db199SXin Li logging.info('Cloning git repo %s', self.giturl) 184*9c5db199SXin Li cmd = 'clone %s %s ' % (self.giturl, self.repodir) 185*9c5db199SXin Li if remote_branch: 186*9c5db199SXin Li cmd += '-b %s' % remote_branch 187*9c5db199SXin Li if shallow: 188*9c5db199SXin Li cmd += '--depth 1' 189*9c5db199SXin Li abs_work_tree = self.work_tree 190*9c5db199SXin Li self.work_tree = None 191*9c5db199SXin Li try: 192*9c5db199SXin Li rv = self.gitcmd(cmd, True) 193*9c5db199SXin Li if rv.exit_status != 0: 194*9c5db199SXin Li logging.error(rv.stderr) 195*9c5db199SXin Li raise GitCloneError('Failed to clone git url', rv) 196*9c5db199SXin Li else: 197*9c5db199SXin Li logging.info(rv.stdout) 198*9c5db199SXin Li finally: 199*9c5db199SXin Li self.work_tree = abs_work_tree 200*9c5db199SXin Li 201*9c5db199SXin Li 202*9c5db199SXin Li def pull(self, rebase=False): 203*9c5db199SXin Li """ 204*9c5db199SXin Li Pulls into repodir using giturl. 205*9c5db199SXin Li 206*9c5db199SXin Li @param rebase: If true forces git pull to perform a rebase instead of a 207*9c5db199SXin Li merge. 208*9c5db199SXin Li @raises GitPullError: if pulling from giturl fails. 209*9c5db199SXin Li """ 210*9c5db199SXin Li logging.info('Updating git repo %s', self.giturl) 211*9c5db199SXin Li cmd = 'pull ' 212*9c5db199SXin Li if rebase: 213*9c5db199SXin Li cmd += '--rebase ' 214*9c5db199SXin Li cmd += self.giturl 215*9c5db199SXin Li 216*9c5db199SXin Li rv = self.gitcmd(cmd, True) 217*9c5db199SXin Li if rv.exit_status != 0: 218*9c5db199SXin Li logging.error(rv.stderr) 219*9c5db199SXin Li e_msg = 'Failed to pull git repo data' 220*9c5db199SXin Li raise GitPullError(e_msg, rv) 221*9c5db199SXin Li 222*9c5db199SXin Li 223*9c5db199SXin Li def commit(self, msg='default'): 224*9c5db199SXin Li """ 225*9c5db199SXin Li Commit changes to repo with the supplied commit msg. 226*9c5db199SXin Li 227*9c5db199SXin Li @param msg: A message that goes with the commit. 228*9c5db199SXin Li """ 229*9c5db199SXin Li rv = self.gitcmd('commit -a -m \'%s\'' % msg) 230*9c5db199SXin Li if rv.exit_status != 0: 231*9c5db199SXin Li logging.error(rv.stderr) 232*9c5db199SXin Li raise GitCommitError('Unable to commit', rv) 233*9c5db199SXin Li 234*9c5db199SXin Li 235*9c5db199SXin Li def upload_cl(self, remote, remote_branch, local_ref='HEAD', draft=False, 236*9c5db199SXin Li dryrun=False): 237*9c5db199SXin Li """ 238*9c5db199SXin Li Upload the change. 239*9c5db199SXin Li 240*9c5db199SXin Li @param remote: The git remote to upload the CL. 241*9c5db199SXin Li @param remote_branch: The remote branch to upload the CL. 242*9c5db199SXin Li @param local_ref: The local ref to upload. 243*9c5db199SXin Li @param draft: Whether to upload the CL as a draft. 244*9c5db199SXin Li @param dryrun: Whether the upload operation is a dryrun. 245*9c5db199SXin Li 246*9c5db199SXin Li @return: Git command result stderr. 247*9c5db199SXin Li """ 248*9c5db199SXin Li remote_refspec = (('refs/drafts/%s' if draft else 'refs/for/%s') % 249*9c5db199SXin Li remote_branch) 250*9c5db199SXin Li return self.push(remote, local_ref, remote_refspec, dryrun=dryrun) 251*9c5db199SXin Li 252*9c5db199SXin Li 253*9c5db199SXin Li def push(self, remote, local_refspec, remote_refspec, dryrun=False): 254*9c5db199SXin Li """ 255*9c5db199SXin Li Push the change. 256*9c5db199SXin Li 257*9c5db199SXin Li @param remote: The git remote to push the CL. 258*9c5db199SXin Li @param local_ref: The local ref to push. 259*9c5db199SXin Li @param remote_refspec: The remote ref to push to. 260*9c5db199SXin Li @param dryrun: Whether the upload operation is a dryrun. 261*9c5db199SXin Li 262*9c5db199SXin Li @return: Git command result stderr. 263*9c5db199SXin Li """ 264*9c5db199SXin Li cmd = 'push %s %s:%s' % (remote, local_refspec, remote_refspec) 265*9c5db199SXin Li 266*9c5db199SXin Li if dryrun: 267*9c5db199SXin Li logging.info('Would run push command: %s.', cmd) 268*9c5db199SXin Li return 269*9c5db199SXin Li 270*9c5db199SXin Li rv = self.gitcmd(cmd) 271*9c5db199SXin Li if rv.exit_status != 0: 272*9c5db199SXin Li logging.error(rv.stderr) 273*9c5db199SXin Li raise GitPushError('Unable to push', rv) 274*9c5db199SXin Li 275*9c5db199SXin Li # The CL url is in the result stderr (not stdout) 276*9c5db199SXin Li return rv.stderr 277*9c5db199SXin Li 278*9c5db199SXin Li 279*9c5db199SXin Li def reset(self, branch_or_sha): 280*9c5db199SXin Li """ 281*9c5db199SXin Li Reset repo to the given branch or git sha. 282*9c5db199SXin Li 283*9c5db199SXin Li @param branch_or_sha: Name of a local or remote branch or git sha. 284*9c5db199SXin Li 285*9c5db199SXin Li @raises GitResetError if operation fails. 286*9c5db199SXin Li """ 287*9c5db199SXin Li self.gitcmd('reset --hard %s' % branch_or_sha, 288*9c5db199SXin Li error_class=GitResetError, 289*9c5db199SXin Li error_msg='Failed to reset to %s' % branch_or_sha) 290*9c5db199SXin Li 291*9c5db199SXin Li 292*9c5db199SXin Li def reset_head(self): 293*9c5db199SXin Li """ 294*9c5db199SXin Li Reset repo to HEAD@{0} by running git reset --hard HEAD. 295*9c5db199SXin Li 296*9c5db199SXin Li TODO(pprabhu): cleanup. Use reset. 297*9c5db199SXin Li 298*9c5db199SXin Li @raises GitResetError: if we fails to reset HEAD. 299*9c5db199SXin Li """ 300*9c5db199SXin Li logging.info('Resetting head on repo %s', self.repodir) 301*9c5db199SXin Li rv = self.gitcmd('reset --hard HEAD') 302*9c5db199SXin Li if rv.exit_status != 0: 303*9c5db199SXin Li logging.error(rv.stderr) 304*9c5db199SXin Li e_msg = 'Failed to reset HEAD' 305*9c5db199SXin Li raise GitResetError(e_msg, rv) 306*9c5db199SXin Li 307*9c5db199SXin Li 308*9c5db199SXin Li def fetch_remote(self): 309*9c5db199SXin Li """ 310*9c5db199SXin Li Fetches all files from the remote but doesn't reset head. 311*9c5db199SXin Li 312*9c5db199SXin Li @raises GitFetchError: if we fail to fetch all files from giturl. 313*9c5db199SXin Li """ 314*9c5db199SXin Li logging.info('fetching from repo %s', self.giturl) 315*9c5db199SXin Li rv = self.gitcmd('fetch --all') 316*9c5db199SXin Li if rv.exit_status != 0: 317*9c5db199SXin Li logging.error(rv.stderr) 318*9c5db199SXin Li e_msg = 'Failed to fetch from %s' % self.giturl 319*9c5db199SXin Li raise GitFetchError(e_msg, rv) 320*9c5db199SXin Li 321*9c5db199SXin Li 322*9c5db199SXin Li def reinit_repo_at(self, remote_branch): 323*9c5db199SXin Li """ 324*9c5db199SXin Li Does all it can to ensure that the repo is at remote_branch. 325*9c5db199SXin Li 326*9c5db199SXin Li This will try to be nice and detect any local changes and bail early. 327*9c5db199SXin Li OTOH, if it finishes successfully, it'll blow away anything and 328*9c5db199SXin Li everything so that local repo reflects the upstream branch requested. 329*9c5db199SXin Li 330*9c5db199SXin Li @param remote_branch: branch to check out. 331*9c5db199SXin Li """ 332*9c5db199SXin Li if not self.is_repo_initialized(): 333*9c5db199SXin Li self.clone() 334*9c5db199SXin Li 335*9c5db199SXin Li # Play nice. Detect any local changes and bail. 336*9c5db199SXin Li # Re-stat all files before comparing index. This is needed for 337*9c5db199SXin Li # diff-index to work properly in cases when the stat info on files is 338*9c5db199SXin Li # stale. (e.g., you just untarred the whole git folder that you got from 339*9c5db199SXin Li # Alice) 340*9c5db199SXin Li rv = self.gitcmd('update-index --refresh -q', 341*9c5db199SXin Li error_class=GitError, 342*9c5db199SXin Li error_msg='Failed to refresh index.') 343*9c5db199SXin Li rv = self.gitcmd( 344*9c5db199SXin Li 'diff-index --quiet HEAD --', 345*9c5db199SXin Li error_class=GitError, 346*9c5db199SXin Li error_msg='Failed to check for local changes.') 347*9c5db199SXin Li if rv.stdout: 348*9c5db199SXin Li logging.error(rv.stdout) 349*9c5db199SXin Li e_msg = 'Local checkout dirty. (%s)' 350*9c5db199SXin Li raise GitError(e_msg % rv.stdout) 351*9c5db199SXin Li 352*9c5db199SXin Li # Play the bad cop. Destroy everything in your path. 353*9c5db199SXin Li # Don't trust the existing repo setup at all (so don't trust the current 354*9c5db199SXin Li # config, current branches / remotes etc). 355*9c5db199SXin Li self.gitcmd('config remote.origin.url %s' % self.giturl, 356*9c5db199SXin Li error_class=GitError, 357*9c5db199SXin Li error_msg='Failed to set origin.') 358*9c5db199SXin Li self.gitcmd('checkout -f', 359*9c5db199SXin Li error_class=GitError, 360*9c5db199SXin Li error_msg='Failed to checkout.') 361*9c5db199SXin Li self.gitcmd('clean -qxdf', 362*9c5db199SXin Li error_class=GitError, 363*9c5db199SXin Li error_msg='Failed to clean.') 364*9c5db199SXin Li self.fetch_remote() 365*9c5db199SXin Li self.reset('origin/%s' % remote_branch) 366*9c5db199SXin Li 367*9c5db199SXin Li 368*9c5db199SXin Li def get(self, **kwargs): 369*9c5db199SXin Li """ 370*9c5db199SXin Li This method overrides baseclass get so we can do proper git 371*9c5db199SXin Li clone/pulls, and check for updated versions. The result of 372*9c5db199SXin Li this method will leave an up-to-date version of git repo at 373*9c5db199SXin Li 'giturl' in 'repodir' directory to be used by build/install 374*9c5db199SXin Li methods. 375*9c5db199SXin Li 376*9c5db199SXin Li @param kwargs: Dictionary of parameters to the method get. 377*9c5db199SXin Li """ 378*9c5db199SXin Li if not self.is_repo_initialized(): 379*9c5db199SXin Li # this is your first time ... 380*9c5db199SXin Li self.clone() 381*9c5db199SXin Li elif self.is_out_of_date(): 382*9c5db199SXin Li # exiting repo, check if we're up-to-date 383*9c5db199SXin Li self.pull() 384*9c5db199SXin Li else: 385*9c5db199SXin Li logging.info('repo up-to-date') 386*9c5db199SXin Li 387*9c5db199SXin Li # remember where the source is 388*9c5db199SXin Li self.source_material = self.repodir 389*9c5db199SXin Li 390*9c5db199SXin Li 391*9c5db199SXin Li def get_local_head(self): 392*9c5db199SXin Li """ 393*9c5db199SXin Li Get the top commit hash of the current local git branch. 394*9c5db199SXin Li 395*9c5db199SXin Li @return: Top commit hash of local git branch 396*9c5db199SXin Li """ 397*9c5db199SXin Li cmd = 'log --pretty=format:"%H" -1' 398*9c5db199SXin Li l_head_cmd = self.gitcmd(cmd) 399*9c5db199SXin Li return l_head_cmd.stdout.strip() 400*9c5db199SXin Li 401*9c5db199SXin Li 402*9c5db199SXin Li def get_remote_head(self): 403*9c5db199SXin Li """ 404*9c5db199SXin Li Get the top commit hash of the current remote git branch. 405*9c5db199SXin Li 406*9c5db199SXin Li @return: Top commit hash of remote git branch 407*9c5db199SXin Li """ 408*9c5db199SXin Li cmd1 = 'remote show' 409*9c5db199SXin Li origin_name_cmd = self.gitcmd(cmd1) 410*9c5db199SXin Li cmd2 = 'log --pretty=format:"%H" -1 ' + origin_name_cmd.stdout.strip() 411*9c5db199SXin Li r_head_cmd = self.gitcmd(cmd2) 412*9c5db199SXin Li return r_head_cmd.stdout.strip() 413*9c5db199SXin Li 414*9c5db199SXin Li 415*9c5db199SXin Li def is_out_of_date(self): 416*9c5db199SXin Li """ 417*9c5db199SXin Li Return whether this branch is out of date with regards to remote branch. 418*9c5db199SXin Li 419*9c5db199SXin Li @return: False, if the branch is outdated, True if it is current. 420*9c5db199SXin Li """ 421*9c5db199SXin Li local_head = self.get_local_head() 422*9c5db199SXin Li remote_head = self.get_remote_head() 423*9c5db199SXin Li 424*9c5db199SXin Li # local is out-of-date, pull 425*9c5db199SXin Li if local_head != remote_head: 426*9c5db199SXin Li return True 427*9c5db199SXin Li 428*9c5db199SXin Li return False 429*9c5db199SXin Li 430*9c5db199SXin Li 431*9c5db199SXin Li def is_repo_initialized(self): 432*9c5db199SXin Li """ 433*9c5db199SXin Li Return whether the git repo was already initialized. 434*9c5db199SXin Li 435*9c5db199SXin Li Counts objects in .git directory, since these will exist even if the 436*9c5db199SXin Li repo is empty. Assumes non-bare reposotories like the rest of this file. 437*9c5db199SXin Li 438*9c5db199SXin Li @return: True if the repo is initialized. 439*9c5db199SXin Li """ 440*9c5db199SXin Li cmd = 'count-objects' 441*9c5db199SXin Li rv = self.gitcmd(cmd, True) 442*9c5db199SXin Li if rv.exit_status == 0: 443*9c5db199SXin Li return True 444*9c5db199SXin Li 445*9c5db199SXin Li return False 446*9c5db199SXin Li 447*9c5db199SXin Li 448*9c5db199SXin Li def get_latest_commit_hash(self): 449*9c5db199SXin Li """ 450*9c5db199SXin Li Get the commit hash of the latest commit in the repo. 451*9c5db199SXin Li 452*9c5db199SXin Li We don't raise an exception if no commit hash was found as 453*9c5db199SXin Li this could be an empty repository. The caller should notice this 454*9c5db199SXin Li methods return value and raise one appropriately. 455*9c5db199SXin Li 456*9c5db199SXin Li @return: The first commit hash if anything has been committed. 457*9c5db199SXin Li """ 458*9c5db199SXin Li cmd = 'rev-list -n 1 --all' 459*9c5db199SXin Li rv = self.gitcmd(cmd, True) 460*9c5db199SXin Li if rv.exit_status == 0: 461*9c5db199SXin Li return rv.stdout 462*9c5db199SXin Li return None 463*9c5db199SXin Li 464*9c5db199SXin Li 465*9c5db199SXin Li def is_repo_empty(self): 466*9c5db199SXin Li """ 467*9c5db199SXin Li Checks for empty but initialized repos. 468*9c5db199SXin Li 469*9c5db199SXin Li eg: we clone an empty main repo, then don't pull 470*9c5db199SXin Li after the main commits. 471*9c5db199SXin Li 472*9c5db199SXin Li @return True if the repo has no commits. 473*9c5db199SXin Li """ 474*9c5db199SXin Li if self.get_latest_commit_hash(): 475*9c5db199SXin Li return False 476*9c5db199SXin Li return True 477*9c5db199SXin Li 478*9c5db199SXin Li 479*9c5db199SXin Li def get_revision(self): 480*9c5db199SXin Li """ 481*9c5db199SXin Li Return current HEAD commit id 482*9c5db199SXin Li """ 483*9c5db199SXin Li if not self.is_repo_initialized(): 484*9c5db199SXin Li self.get() 485*9c5db199SXin Li 486*9c5db199SXin Li cmd = 'rev-parse --verify HEAD' 487*9c5db199SXin Li gitlog = self.gitcmd(cmd, True) 488*9c5db199SXin Li if gitlog.exit_status != 0: 489*9c5db199SXin Li logging.error(gitlog.stderr) 490*9c5db199SXin Li raise error.CmdError('Failed to find git sha1 revision', gitlog) 491*9c5db199SXin Li else: 492*9c5db199SXin Li return gitlog.stdout.strip('\n') 493*9c5db199SXin Li 494*9c5db199SXin Li 495*9c5db199SXin Li def checkout(self, remote, local=None): 496*9c5db199SXin Li """ 497*9c5db199SXin Li Check out the git commit id, branch, or tag given by remote. 498*9c5db199SXin Li 499*9c5db199SXin Li Optional give the local branch name as local. 500*9c5db199SXin Li 501*9c5db199SXin Li @param remote: Remote commit hash 502*9c5db199SXin Li @param local: Local commit hash 503*9c5db199SXin Li @note: For git checkout tag git version >= 1.5.0 is required 504*9c5db199SXin Li """ 505*9c5db199SXin Li if not self.is_repo_initialized(): 506*9c5db199SXin Li self.get() 507*9c5db199SXin Li 508*9c5db199SXin Li assert(isinstance(remote, six.string_types)) 509*9c5db199SXin Li if local: 510*9c5db199SXin Li cmd = 'checkout -b %s %s' % (local, remote) 511*9c5db199SXin Li else: 512*9c5db199SXin Li cmd = 'checkout %s' % (remote) 513*9c5db199SXin Li gitlog = self.gitcmd(cmd, True) 514*9c5db199SXin Li if gitlog.exit_status != 0: 515*9c5db199SXin Li logging.error(gitlog.stderr) 516*9c5db199SXin Li raise error.CmdError('Failed to checkout git branch', gitlog) 517*9c5db199SXin Li else: 518*9c5db199SXin Li logging.info(gitlog.stdout) 519*9c5db199SXin Li 520*9c5db199SXin Li 521*9c5db199SXin Li def get_branch(self, all=False, remote_tracking=False): 522*9c5db199SXin Li """ 523*9c5db199SXin Li Show the branches. 524*9c5db199SXin Li 525*9c5db199SXin Li @param all: List both remote-tracking branches and local branches (True) 526*9c5db199SXin Li or only the local ones (False). 527*9c5db199SXin Li @param remote_tracking: Lists the remote-tracking branches. 528*9c5db199SXin Li """ 529*9c5db199SXin Li if not self.is_repo_initialized(): 530*9c5db199SXin Li self.get() 531*9c5db199SXin Li 532*9c5db199SXin Li cmd = 'branch --no-color' 533*9c5db199SXin Li if all: 534*9c5db199SXin Li cmd = " ".join([cmd, "-a"]) 535*9c5db199SXin Li if remote_tracking: 536*9c5db199SXin Li cmd = " ".join([cmd, "-r"]) 537*9c5db199SXin Li 538*9c5db199SXin Li gitlog = self.gitcmd(cmd, True) 539*9c5db199SXin Li if gitlog.exit_status != 0: 540*9c5db199SXin Li logging.error(gitlog.stderr) 541*9c5db199SXin Li raise error.CmdError('Failed to get git branch', gitlog) 542*9c5db199SXin Li elif all or remote_tracking: 543*9c5db199SXin Li return gitlog.stdout.strip('\n') 544*9c5db199SXin Li else: 545*9c5db199SXin Li branch = [b[2:] for b in gitlog.stdout.split('\n') 546*9c5db199SXin Li if b.startswith('*')][0] 547*9c5db199SXin Li return branch 548*9c5db199SXin Li 549*9c5db199SXin Li 550*9c5db199SXin Li def status(self, short=True): 551*9c5db199SXin Li """ 552*9c5db199SXin Li Return the current status of the git repo. 553*9c5db199SXin Li 554*9c5db199SXin Li @param short: Whether to give the output in the short-format. 555*9c5db199SXin Li """ 556*9c5db199SXin Li cmd = 'status' 557*9c5db199SXin Li 558*9c5db199SXin Li if short: 559*9c5db199SXin Li cmd += ' -s' 560*9c5db199SXin Li 561*9c5db199SXin Li gitlog = self.gitcmd(cmd, True) 562*9c5db199SXin Li if gitlog.exit_status != 0: 563*9c5db199SXin Li logging.error(gitlog.stderr) 564*9c5db199SXin Li raise error.CmdError('Failed to get git status', gitlog) 565*9c5db199SXin Li else: 566*9c5db199SXin Li return gitlog.stdout.strip('\n') 567*9c5db199SXin Li 568*9c5db199SXin Li 569*9c5db199SXin Li def config(self, option_name): 570*9c5db199SXin Li """ 571*9c5db199SXin Li Return the git config value for the given option name. 572*9c5db199SXin Li 573*9c5db199SXin Li @option_name: The name of the git option to get. 574*9c5db199SXin Li """ 575*9c5db199SXin Li cmd = 'config ' + option_name 576*9c5db199SXin Li gitlog = self.gitcmd(cmd) 577*9c5db199SXin Li 578*9c5db199SXin Li if gitlog.exit_status != 0: 579*9c5db199SXin Li logging.error(gitlog.stderr) 580*9c5db199SXin Li raise error.CmdError('Failed to get git config %', option_name) 581*9c5db199SXin Li else: 582*9c5db199SXin Li return gitlog.stdout.strip('\n') 583*9c5db199SXin Li 584*9c5db199SXin Li 585*9c5db199SXin Li def remote(self): 586*9c5db199SXin Li """ 587*9c5db199SXin Li Return repository git remote name. 588*9c5db199SXin Li """ 589*9c5db199SXin Li gitlog = self.gitcmd('remote') 590*9c5db199SXin Li 591*9c5db199SXin Li if gitlog.exit_status != 0: 592*9c5db199SXin Li logging.error(gitlog.stderr) 593*9c5db199SXin Li raise error.CmdError('Failed to run git remote.') 594*9c5db199SXin Li else: 595*9c5db199SXin Li return gitlog.stdout.strip('\n') 596