xref: /aosp_15_r20/external/autotest/client/common_lib/revision_control.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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