Index: client/common_lib/revision_control.py |
diff --git a/client/common_lib/revision_control.py b/client/common_lib/revision_control.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..931e5a5ca54095d7aa2280d9107b0a912454e343 |
--- /dev/null |
+++ b/client/common_lib/revision_control.py |
@@ -0,0 +1,232 @@ |
+""" |
+Module with abstraction layers to revision control systems. |
+ |
+With this library, autotest developers can handle source code checkouts and |
+updates on both client as well as server code. |
+""" |
+ |
+import os, warnings, logging |
+import error, utils |
+from autotest_lib.client.bin import os_dep |
+ |
+ |
+class GitRepo(object): |
+ """ |
+ This class represents a git repo. |
+ |
+ It is used to pull down a local copy of a git repo, check if the local |
+ repo is up-to-date, if not update. It delegates the install to |
+ implementation classes. |
+ """ |
+ |
+ def __init__(self, repodir, giturl, weburl=None): |
+ if repodir is None: |
+ raise ValueError('You must provide a path that will hold the' |
+ 'git repository') |
+ self.repodir = utils.sh_escape(repodir) |
+ if giturl is None: |
+ raise ValueError('You must provide a git URL repository') |
+ self.giturl = giturl |
+ if weburl is not None: |
+ warnings.warn("Param weburl: You are no longer required to provide " |
+ "a web URL for your git repos", DeprecationWarning) |
+ |
+ # path to .git dir |
+ self.gitpath = utils.sh_escape(os.path.join(self.repodir,'.git')) |
+ |
+ # Find git base command. If not found, this will throw an exception |
+ git_base_cmd = os_dep.command('git') |
+ |
+ # base git command , pointing to gitpath git dir |
+ self.gitcmdbase = '%s --git-dir=%s' % (git_base_cmd, self.gitpath) |
+ |
+ # default to same remote path as local |
+ self._build = os.path.dirname(self.repodir) |
+ |
+ |
+ def _run(self, command, timeout=None, ignore_status=False): |
+ """ |
+ Auxiliary function to run a command, with proper shell escaping. |
+ |
+ @param timeout: Timeout to run the command. |
+ @param ignore_status: Whether we should supress error.CmdError |
+ exceptions if the command did return exit code !=0 (True), or |
+ not supress them (False). |
+ """ |
+ return utils.run(r'%s' % (utils.sh_escape(command)), |
+ timeout, ignore_status) |
+ |
+ |
+ def gitcmd(self, cmd, ignore_status=False): |
+ """ |
+ Wrapper for a git command. |
+ |
+ @param cmd: Git subcommand (ex 'clone'). |
+ @param ignore_status: Whether we should supress error.CmdError |
+ exceptions if the command did return exit code !=0 (True), or |
+ not supress them (False). |
+ """ |
+ cmd = '%s %s' % (self.gitcmdbase, cmd) |
+ return self._run(cmd, ignore_status=ignore_status) |
+ |
+ |
+ def get(self, **kwargs): |
+ """ |
+ This method overrides baseclass get so we can do proper git |
+ clone/pulls, and check for updated versions. The result of |
+ this method will leave an up-to-date version of git repo at |
+ 'giturl' in 'repodir' directory to be used by build/install |
+ methods. |
+ |
+ @param **kwargs: Dictionary of parameters to the method get. |
+ """ |
+ if not self.is_repo_initialized(): |
+ # this is your first time ... |
+ logging.info('Cloning git repo %s', self.giturl) |
+ cmd = 'clone %s %s ' % (self.giturl, self.repodir) |
+ rv = self.gitcmd(cmd, True) |
+ if rv.exit_status != 0: |
+ logging.error(rv.stderr) |
+ raise error.CmdError('Failed to clone git url', rv) |
+ else: |
+ logging.info(rv.stdout) |
+ |
+ else: |
+ # exiting repo, check if we're up-to-date |
+ if self.is_out_of_date(): |
+ logging.info('Updating git repo %s', self.giturl) |
+ rv = self.gitcmd('pull', True) |
+ if rv.exit_status != 0: |
+ logging.error(rv.stderr) |
+ e_msg = 'Failed to pull git repo data' |
+ raise error.CmdError(e_msg, rv) |
+ else: |
+ logging.info('repo up-to-date') |
+ |
+ # remember where the source is |
+ self.source_material = self.repodir |
+ |
+ |
+ def get_local_head(self): |
+ """ |
+ Get the top commit hash of the current local git branch. |
+ |
+ @return: Top commit hash of local git branch |
+ """ |
+ cmd = 'log --pretty=format:"%H" -1' |
+ l_head_cmd = self.gitcmd(cmd) |
+ return l_head_cmd.stdout.strip() |
+ |
+ |
+ def get_remote_head(self): |
+ """ |
+ Get the top commit hash of the current remote git branch. |
+ |
+ @return: Top commit hash of remote git branch |
+ """ |
+ cmd1 = 'remote show' |
+ origin_name_cmd = self.gitcmd(cmd1) |
+ cmd2 = 'log --pretty=format:"%H" -1 ' + origin_name_cmd.stdout.strip() |
+ r_head_cmd = self.gitcmd(cmd2) |
+ return r_head_cmd.stdout.strip() |
+ |
+ |
+ def is_out_of_date(self): |
+ """ |
+ Return whether this branch is out of date with regards to remote branch. |
+ |
+ @return: False, if the branch is outdated, True if it is current. |
+ """ |
+ local_head = self.get_local_head() |
+ remote_head = self.get_remote_head() |
+ |
+ # local is out-of-date, pull |
+ if local_head != remote_head: |
+ return True |
+ |
+ return False |
+ |
+ |
+ def is_repo_initialized(self): |
+ """ |
+ Return whether the git repo was already initialized (has a top commit). |
+ |
+ @return: False, if the repo was initialized, True if it was not. |
+ """ |
+ cmd = 'log --max-count=1' |
+ rv = self.gitcmd(cmd, True) |
+ if rv.exit_status == 0: |
+ return True |
+ |
+ return False |
+ |
+ |
+ def get_revision(self): |
+ """ |
+ Return current HEAD commit id |
+ """ |
+ if not self.is_repo_initialized(): |
+ self.get() |
+ |
+ cmd = 'rev-parse --verify HEAD' |
+ gitlog = self.gitcmd(cmd, True) |
+ if gitlog.exit_status != 0: |
+ logging.error(gitlog.stderr) |
+ raise error.CmdError('Failed to find git sha1 revision', gitlog) |
+ else: |
+ return gitlog.stdout.strip('\n') |
+ |
+ |
+ def checkout(self, remote, local=None): |
+ """ |
+ Check out the git commit id, branch, or tag given by remote. |
+ |
+ Optional give the local branch name as local. |
+ |
+ @param remote: Remote commit hash |
+ @param local: Local commit hash |
+ @note: For git checkout tag git version >= 1.5.0 is required |
+ """ |
+ if not self.is_repo_initialized(): |
+ self.get() |
+ |
+ assert(isinstance(remote, basestring)) |
+ if local: |
+ cmd = 'checkout -b %s %s' % (local, remote) |
+ else: |
+ cmd = 'checkout %s' % (remote) |
+ gitlog = self.gitcmd(cmd, True) |
+ if gitlog.exit_status != 0: |
+ logging.error(gitlog.stderr) |
+ raise error.CmdError('Failed to checkout git branch', gitlog) |
+ else: |
+ logging.info(gitlog.stdout) |
+ |
+ |
+ def get_branch(self, all=False, remote_tracking=False): |
+ """ |
+ Show the branches. |
+ |
+ @param all: List both remote-tracking branches and local branches (True) |
+ or only the local ones (False). |
+ @param remote_tracking: Lists the remote-tracking branches. |
+ """ |
+ if not self.is_repo_initialized(): |
+ self.get() |
+ |
+ cmd = 'branch --no-color' |
+ if all: |
+ cmd = " ".join([cmd, "-a"]) |
+ if remote_tracking: |
+ cmd = " ".join([cmd, "-r"]) |
+ |
+ gitlog = self.gitcmd(cmd, True) |
+ if gitlog.exit_status != 0: |
+ logging.error(gitlog.stderr) |
+ raise error.CmdError('Failed to get git branch', gitlog) |
+ elif all or remote_tracking: |
+ return gitlog.stdout.strip('\n') |
+ else: |
+ branch = [b[2:] for b in gitlog.stdout.split('\n') |
+ if b.startswith('*')][0] |
+ return branch |