blob: 41cc51e574583ecaa16421913fcbf0a6c20e9b0f [file] [log] [blame]
Eric Li6f27d4f2010-09-29 10:55:17 -07001"""
2Module with abstraction layers to revision control systems.
3
4With this library, autotest developers can handle source code checkouts and
5updates on both client as well as server code.
6"""
7
8import os, warnings, logging
9import error, utils
10from autotest_lib.client.bin import os_dep
11
12
beepsd9153b52013-01-23 20:52:46 -080013class RevisionControlError(Exception):
14 """Local exception to be raised by code in this file."""
15
16
17class GitError(RevisionControlError):
18 """Exceptions raised for general git errors."""
19
20
21class GitCloneError(GitError):
22 """Exceptions raised for git clone errors."""
23
24
25class GitFetchError(GitError):
26 """Exception raised for git fetch errors."""
27
28
29class GitPullError(GitError):
30 """Exception raised for git pull errors."""
31
32
33class GitResetError(GitError):
34 """Exception raised for git reset errors."""
35
36
37class GitCommitError(GitError):
38 """Exception raised for git commit errors."""
39
40
Eric Li6f27d4f2010-09-29 10:55:17 -070041class GitRepo(object):
42 """
43 This class represents a git repo.
44
45 It is used to pull down a local copy of a git repo, check if the local
46 repo is up-to-date, if not update. It delegates the install to
47 implementation classes.
48 """
49
beeps98365d82013-02-20 20:08:07 -080050 def __init__(self, repodir, giturl=None, weburl=None, abs_work_tree=None):
beepsd9153b52013-01-23 20:52:46 -080051 """
52 Initialized reposotory.
53
54 @param repodir: destination repo directory.
55 @param giturl: master repo git url.
56 @param weburl: a web url for the master repo.
57 @param abs_work_tree: work tree of the git repo. In the
58 absence of a work tree git manipulations will occur
59 in the current working directory for non bare repos.
60 In such repos the -git-dir option should point to
61 the .git directory and -work-tree should point to
62 the repos working tree.
63 Note: a bare reposotory is one which contains all the
64 working files (the tree) and the other wise hidden files
65 (.git) in the same directory. This class assumes non-bare
66 reposotories.
67 """
Eric Li6f27d4f2010-09-29 10:55:17 -070068 if repodir is None:
69 raise ValueError('You must provide a path that will hold the'
70 'git repository')
71 self.repodir = utils.sh_escape(repodir)
beeps98365d82013-02-20 20:08:07 -080072 self._giturl = giturl
Eric Li6f27d4f2010-09-29 10:55:17 -070073 if weburl is not None:
74 warnings.warn("Param weburl: You are no longer required to provide "
75 "a web URL for your git repos", DeprecationWarning)
76
77 # path to .git dir
78 self.gitpath = utils.sh_escape(os.path.join(self.repodir,'.git'))
79
80 # Find git base command. If not found, this will throw an exception
beepsd9153b52013-01-23 20:52:46 -080081 self.git_base_cmd = os_dep.command('git')
beepsd9153b52013-01-23 20:52:46 -080082 self.work_tree = abs_work_tree
Eric Li6f27d4f2010-09-29 10:55:17 -070083
84 # default to same remote path as local
85 self._build = os.path.dirname(self.repodir)
86
87
beeps98365d82013-02-20 20:08:07 -080088 @property
89 def giturl(self):
90 """
91 A giturl is necessary to perform certain actions (clone, pull, fetch)
92 but not others (like diff).
93 """
94 if self._giturl is None:
95 raise ValueError('Unsupported operation -- this object was not'
96 'constructed with a git URL.')
97 return self._giturl
98
99
beepsd9153b52013-01-23 20:52:46 -0800100 def gen_git_cmd_base(self):
101 """
102 The command we use to run git cannot be set. It is reconstructed
103 on each access from it's component variables. This is it's getter.
104 """
105 # base git command , pointing to gitpath git dir
106 gitcmdbase = '%s --git-dir=%s' % (self.git_base_cmd,
107 self.gitpath)
108 if self.work_tree:
109 gitcmdbase += ' --work-tree=%s' % self.work_tree
110 return gitcmdbase
111
112
Eric Li6f27d4f2010-09-29 10:55:17 -0700113 def _run(self, command, timeout=None, ignore_status=False):
114 """
115 Auxiliary function to run a command, with proper shell escaping.
116
117 @param timeout: Timeout to run the command.
118 @param ignore_status: Whether we should supress error.CmdError
119 exceptions if the command did return exit code !=0 (True), or
120 not supress them (False).
121 """
122 return utils.run(r'%s' % (utils.sh_escape(command)),
123 timeout, ignore_status)
124
125
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700126 def gitcmd(self, cmd, ignore_status=False, error_class=None,
127 error_msg=None):
Eric Li6f27d4f2010-09-29 10:55:17 -0700128 """
129 Wrapper for a git command.
130
131 @param cmd: Git subcommand (ex 'clone').
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700132 @param ignore_status: If True, ignore the CmdError raised by the
133 underlying command runner. NB: Passing in an error_class
134 impiles ignore_status=True.
135 @param error_class: When ignore_status is False, optional error
136 error class to log and raise in case of errors. Must be a
137 (sub)type of GitError.
138 @param error_msg: When passed with error_class, used as a friendly
139 error message.
Eric Li6f27d4f2010-09-29 10:55:17 -0700140 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700141 # TODO(pprabhu) Get rid of the ignore_status argument.
142 # Now that we support raising custom errors, we always want to get a
143 # return code from the command execution, instead of an exception.
144 ignore_status = ignore_status or error_class is not None
beepsd9153b52013-01-23 20:52:46 -0800145 cmd = '%s %s' % (self.gen_git_cmd_base(), cmd)
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700146 rv = self._run(cmd, ignore_status=ignore_status)
147 if rv.exit_status != 0 and error_class is not None:
148 logging.error('git command failed: %s: %s',
149 cmd, error_msg if error_msg is not None else '')
150 logging.error(rv.stderr)
151 raise error_class(error_msg if error_msg is not None
152 else rv.stderr)
153
154 return rv
Eric Li6f27d4f2010-09-29 10:55:17 -0700155
156
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800157 def clone(self, remote_branch=None):
beepsd9153b52013-01-23 20:52:46 -0800158 """
159 Clones a repo using giturl and repodir.
160
161 Since we're cloning the master repo we don't have a work tree yet,
162 make sure the getter of the gitcmd doesn't think we do by setting
163 work_tree to None.
164
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800165 @param remote_branch: Specify the remote branch to clone. None if to
166 clone master branch.
167
beepsd9153b52013-01-23 20:52:46 -0800168 @raises GitCloneError: if cloning the master repo fails.
169 """
170 logging.info('Cloning git repo %s', self.giturl)
171 cmd = 'clone %s %s ' % (self.giturl, self.repodir)
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800172 if remote_branch:
173 cmd += '-b %s' % remote_branch
beepsd9153b52013-01-23 20:52:46 -0800174 abs_work_tree = self.work_tree
175 self.work_tree = None
176 try:
177 rv = self.gitcmd(cmd, True)
178 if rv.exit_status != 0:
179 logging.error(rv.stderr)
180 raise GitCloneError('Failed to clone git url', rv)
181 else:
182 logging.info(rv.stdout)
183 finally:
184 self.work_tree = abs_work_tree
185
186
beepsaae3f1c2013-03-19 15:49:14 -0700187 def pull(self, rebase=False):
beepsd9153b52013-01-23 20:52:46 -0800188 """
189 Pulls into repodir using giturl.
190
beepsaae3f1c2013-03-19 15:49:14 -0700191 @param rebase: If true forces git pull to perform a rebase instead of a
192 merge.
beepsd9153b52013-01-23 20:52:46 -0800193 @raises GitPullError: if pulling from giturl fails.
194 """
195 logging.info('Updating git repo %s', self.giturl)
beepsaae3f1c2013-03-19 15:49:14 -0700196 cmd = 'pull '
197 if rebase:
198 cmd += '--rebase '
199 cmd += self.giturl
200
beepsd9153b52013-01-23 20:52:46 -0800201 rv = self.gitcmd(cmd, True)
202 if rv.exit_status != 0:
203 logging.error(rv.stderr)
204 e_msg = 'Failed to pull git repo data'
205 raise GitPullError(e_msg, rv)
206
207
208 def commit(self, msg='default'):
209 """
210 Commit changes to repo with the supplied commit msg.
211
212 @param msg: A message that goes with the commit.
213 """
214 rv = self.gitcmd('commit -a -m %s' % msg)
215 if rv.exit_status != 0:
216 logging.error(rv.stderr)
Allen Li25af06d2016-09-27 12:01:53 -0700217 raise GitCommitError('Unable to commit', rv)
beepsd9153b52013-01-23 20:52:46 -0800218
219
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700220 def reset(self, branch_or_sha):
221 """
222 Reset repo to the given branch or git sha.
223
224 @param branch_or_sha: Name of a local or remote branch or git sha.
225
226 @raises GitResetError if operation fails.
227 """
228 self.gitcmd('reset --hard %s' % branch_or_sha,
229 error_class=GitResetError,
230 error_msg='Failed to reset to %s' % branch_or_sha)
231
232
beepsd9153b52013-01-23 20:52:46 -0800233 def reset_head(self):
234 """
235 Reset repo to HEAD@{0} by running git reset --hard HEAD.
236
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700237 TODO(pprabhu): cleanup. Use reset.
238
beepsd9153b52013-01-23 20:52:46 -0800239 @raises GitResetError: if we fails to reset HEAD.
240 """
241 logging.info('Resetting head on repo %s', self.repodir)
242 rv = self.gitcmd('reset --hard HEAD')
243 if rv.exit_status != 0:
244 logging.error(rv.stderr)
245 e_msg = 'Failed to reset HEAD'
246 raise GitResetError(e_msg, rv)
247
248
249 def fetch_remote(self):
250 """
251 Fetches all files from the remote but doesn't reset head.
252
253 @raises GitFetchError: if we fail to fetch all files from giturl.
254 """
255 logging.info('fetching from repo %s', self.giturl)
256 rv = self.gitcmd('fetch --all')
257 if rv.exit_status != 0:
258 logging.error(rv.stderr)
259 e_msg = 'Failed to fetch from %s' % self.giturl
260 raise GitFetchError(e_msg, rv)
261
262
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700263 def reinit_repo_at(self, remote_branch):
beepsd9153b52013-01-23 20:52:46 -0800264 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700265 Does all it can to ensure that the repo is at remote_branch.
266
267 This will try to be nice and detect any local changes and bail early.
268 OTOH, if it finishes successfully, it'll blow away anything and
269 everything so that local repo reflects the upstream branch requested.
Allen Li25af06d2016-09-27 12:01:53 -0700270
271 @param remote_branch: branch to check out.
beepsd9153b52013-01-23 20:52:46 -0800272 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700273 if not self.is_repo_initialized():
beepsd9153b52013-01-23 20:52:46 -0800274 self.clone()
275
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700276 # Play nice. Detect any local changes and bail.
277 # Re-stat all files before comparing index. This is needed for
278 # diff-index to work properly in cases when the stat info on files is
279 # stale. (e.g., you just untarred the whole git folder that you got from
280 # Alice)
281 rv = self.gitcmd('update-index --refresh -q',
282 error_class=GitError,
283 error_msg='Failed to refresh index.')
284 rv = self.gitcmd(
285 'diff-index --quiet HEAD --',
286 error_class=GitError,
287 error_msg='Failed to check for local changes.')
288 if rv.stdout:
Allen Li25af06d2016-09-27 12:01:53 -0700289 logging.error(rv.stdout)
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700290 e_msg = 'Local checkout dirty. (%s)'
291 raise GitError(e_msg % rv.stdout)
292
293 # Play the bad cop. Destroy everything in your path.
294 # Don't trust the existing repo setup at all (so don't trust the current
295 # config, current branches / remotes etc).
296 self.gitcmd('config remote.origin.url %s' % self.giturl,
297 error_class=GitError,
298 error_msg='Failed to set origin.')
299 self.gitcmd('checkout -f',
300 error_class=GitError,
301 error_msg='Failed to checkout.')
302 self.gitcmd('clean -qxdf',
303 error_class=GitError,
304 error_msg='Failed to clean.')
305 self.fetch_remote()
306 self.reset('origin/%s' % remote_branch)
307
beepsd9153b52013-01-23 20:52:46 -0800308
Eric Li6f27d4f2010-09-29 10:55:17 -0700309 def get(self, **kwargs):
310 """
311 This method overrides baseclass get so we can do proper git
312 clone/pulls, and check for updated versions. The result of
313 this method will leave an up-to-date version of git repo at
314 'giturl' in 'repodir' directory to be used by build/install
315 methods.
316
beeps98365d82013-02-20 20:08:07 -0800317 @param kwargs: Dictionary of parameters to the method get.
Eric Li6f27d4f2010-09-29 10:55:17 -0700318 """
319 if not self.is_repo_initialized():
320 # this is your first time ...
beepsd9153b52013-01-23 20:52:46 -0800321 self.clone()
322 elif self.is_out_of_date():
Eric Li6f27d4f2010-09-29 10:55:17 -0700323 # exiting repo, check if we're up-to-date
beepsd9153b52013-01-23 20:52:46 -0800324 self.pull()
325 else:
326 logging.info('repo up-to-date')
Eric Li6f27d4f2010-09-29 10:55:17 -0700327
328 # remember where the source is
329 self.source_material = self.repodir
330
331
332 def get_local_head(self):
333 """
334 Get the top commit hash of the current local git branch.
335
336 @return: Top commit hash of local git branch
337 """
338 cmd = 'log --pretty=format:"%H" -1'
339 l_head_cmd = self.gitcmd(cmd)
340 return l_head_cmd.stdout.strip()
341
342
343 def get_remote_head(self):
344 """
345 Get the top commit hash of the current remote git branch.
346
347 @return: Top commit hash of remote git branch
348 """
349 cmd1 = 'remote show'
350 origin_name_cmd = self.gitcmd(cmd1)
351 cmd2 = 'log --pretty=format:"%H" -1 ' + origin_name_cmd.stdout.strip()
352 r_head_cmd = self.gitcmd(cmd2)
353 return r_head_cmd.stdout.strip()
354
355
356 def is_out_of_date(self):
357 """
358 Return whether this branch is out of date with regards to remote branch.
359
360 @return: False, if the branch is outdated, True if it is current.
361 """
362 local_head = self.get_local_head()
363 remote_head = self.get_remote_head()
364
365 # local is out-of-date, pull
366 if local_head != remote_head:
367 return True
368
369 return False
370
371
372 def is_repo_initialized(self):
373 """
beepsd9153b52013-01-23 20:52:46 -0800374 Return whether the git repo was already initialized.
Eric Li6f27d4f2010-09-29 10:55:17 -0700375
beepsd9153b52013-01-23 20:52:46 -0800376 Counts objects in .git directory, since these will exist even if the
377 repo is empty. Assumes non-bare reposotories like the rest of this file.
378
379 @return: True if the repo is initialized.
Eric Li6f27d4f2010-09-29 10:55:17 -0700380 """
beepsd9153b52013-01-23 20:52:46 -0800381 cmd = 'count-objects'
Eric Li6f27d4f2010-09-29 10:55:17 -0700382 rv = self.gitcmd(cmd, True)
383 if rv.exit_status == 0:
384 return True
385
386 return False
387
388
beepsd9153b52013-01-23 20:52:46 -0800389 def get_latest_commit_hash(self):
390 """
391 Get the commit hash of the latest commit in the repo.
392
393 We don't raise an exception if no commit hash was found as
394 this could be an empty repository. The caller should notice this
395 methods return value and raise one appropriately.
396
397 @return: The first commit hash if anything has been committed.
398 """
399 cmd = 'rev-list -n 1 --all'
400 rv = self.gitcmd(cmd, True)
401 if rv.exit_status == 0:
402 return rv.stdout
403 return None
404
405
406 def is_repo_empty(self):
407 """
408 Checks for empty but initialized repos.
409
410 eg: we clone an empty master repo, then don't pull
411 after the master commits.
412
413 @return True if the repo has no commits.
414 """
415 if self.get_latest_commit_hash():
416 return False
417 return True
418
419
Eric Li6f27d4f2010-09-29 10:55:17 -0700420 def get_revision(self):
421 """
422 Return current HEAD commit id
423 """
424 if not self.is_repo_initialized():
425 self.get()
426
427 cmd = 'rev-parse --verify HEAD'
428 gitlog = self.gitcmd(cmd, True)
429 if gitlog.exit_status != 0:
430 logging.error(gitlog.stderr)
431 raise error.CmdError('Failed to find git sha1 revision', gitlog)
432 else:
433 return gitlog.stdout.strip('\n')
434
435
436 def checkout(self, remote, local=None):
437 """
438 Check out the git commit id, branch, or tag given by remote.
439
440 Optional give the local branch name as local.
441
442 @param remote: Remote commit hash
443 @param local: Local commit hash
444 @note: For git checkout tag git version >= 1.5.0 is required
445 """
446 if not self.is_repo_initialized():
447 self.get()
448
449 assert(isinstance(remote, basestring))
450 if local:
451 cmd = 'checkout -b %s %s' % (local, remote)
452 else:
453 cmd = 'checkout %s' % (remote)
454 gitlog = self.gitcmd(cmd, True)
455 if gitlog.exit_status != 0:
456 logging.error(gitlog.stderr)
457 raise error.CmdError('Failed to checkout git branch', gitlog)
458 else:
459 logging.info(gitlog.stdout)
460
461
462 def get_branch(self, all=False, remote_tracking=False):
463 """
464 Show the branches.
465
466 @param all: List both remote-tracking branches and local branches (True)
467 or only the local ones (False).
468 @param remote_tracking: Lists the remote-tracking branches.
469 """
470 if not self.is_repo_initialized():
471 self.get()
472
473 cmd = 'branch --no-color'
474 if all:
475 cmd = " ".join([cmd, "-a"])
476 if remote_tracking:
477 cmd = " ".join([cmd, "-r"])
478
479 gitlog = self.gitcmd(cmd, True)
480 if gitlog.exit_status != 0:
481 logging.error(gitlog.stderr)
482 raise error.CmdError('Failed to get git branch', gitlog)
483 elif all or remote_tracking:
484 return gitlog.stdout.strip('\n')
485 else:
486 branch = [b[2:] for b in gitlog.stdout.split('\n')
487 if b.startswith('*')][0]
488 return branch