blob: 3d5c570cfeef50978a1a70c5b4b5087b10dc4fea [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
Ningning Xia84190b82018-04-16 15:01:40 -070041class GitPushError(GitError):
42 """Exception raised for git push errors."""
43
44
Eric Li6f27d4f2010-09-29 10:55:17 -070045class GitRepo(object):
46 """
47 This class represents a git repo.
48
49 It is used to pull down a local copy of a git repo, check if the local
50 repo is up-to-date, if not update. It delegates the install to
51 implementation classes.
52 """
53
beeps98365d82013-02-20 20:08:07 -080054 def __init__(self, repodir, giturl=None, weburl=None, abs_work_tree=None):
beepsd9153b52013-01-23 20:52:46 -080055 """
56 Initialized reposotory.
57
58 @param repodir: destination repo directory.
59 @param giturl: master repo git url.
60 @param weburl: a web url for the master repo.
61 @param abs_work_tree: work tree of the git repo. In the
62 absence of a work tree git manipulations will occur
63 in the current working directory for non bare repos.
64 In such repos the -git-dir option should point to
65 the .git directory and -work-tree should point to
66 the repos working tree.
67 Note: a bare reposotory is one which contains all the
68 working files (the tree) and the other wise hidden files
69 (.git) in the same directory. This class assumes non-bare
70 reposotories.
71 """
Eric Li6f27d4f2010-09-29 10:55:17 -070072 if repodir is None:
73 raise ValueError('You must provide a path that will hold the'
74 'git repository')
75 self.repodir = utils.sh_escape(repodir)
beeps98365d82013-02-20 20:08:07 -080076 self._giturl = giturl
Eric Li6f27d4f2010-09-29 10:55:17 -070077 if weburl is not None:
78 warnings.warn("Param weburl: You are no longer required to provide "
79 "a web URL for your git repos", DeprecationWarning)
80
81 # path to .git dir
82 self.gitpath = utils.sh_escape(os.path.join(self.repodir,'.git'))
83
84 # Find git base command. If not found, this will throw an exception
beepsd9153b52013-01-23 20:52:46 -080085 self.git_base_cmd = os_dep.command('git')
beepsd9153b52013-01-23 20:52:46 -080086 self.work_tree = abs_work_tree
Eric Li6f27d4f2010-09-29 10:55:17 -070087
88 # default to same remote path as local
89 self._build = os.path.dirname(self.repodir)
90
91
beeps98365d82013-02-20 20:08:07 -080092 @property
93 def giturl(self):
94 """
95 A giturl is necessary to perform certain actions (clone, pull, fetch)
96 but not others (like diff).
97 """
98 if self._giturl is None:
99 raise ValueError('Unsupported operation -- this object was not'
100 'constructed with a git URL.')
101 return self._giturl
102
103
beepsd9153b52013-01-23 20:52:46 -0800104 def gen_git_cmd_base(self):
105 """
106 The command we use to run git cannot be set. It is reconstructed
107 on each access from it's component variables. This is it's getter.
108 """
109 # base git command , pointing to gitpath git dir
110 gitcmdbase = '%s --git-dir=%s' % (self.git_base_cmd,
111 self.gitpath)
112 if self.work_tree:
113 gitcmdbase += ' --work-tree=%s' % self.work_tree
114 return gitcmdbase
115
116
Eric Li6f27d4f2010-09-29 10:55:17 -0700117 def _run(self, command, timeout=None, ignore_status=False):
118 """
119 Auxiliary function to run a command, with proper shell escaping.
120
121 @param timeout: Timeout to run the command.
122 @param ignore_status: Whether we should supress error.CmdError
123 exceptions if the command did return exit code !=0 (True), or
124 not supress them (False).
125 """
126 return utils.run(r'%s' % (utils.sh_escape(command)),
127 timeout, ignore_status)
128
129
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700130 def gitcmd(self, cmd, ignore_status=False, error_class=None,
131 error_msg=None):
Eric Li6f27d4f2010-09-29 10:55:17 -0700132 """
133 Wrapper for a git command.
134
135 @param cmd: Git subcommand (ex 'clone').
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700136 @param ignore_status: If True, ignore the CmdError raised by the
137 underlying command runner. NB: Passing in an error_class
138 impiles ignore_status=True.
139 @param error_class: When ignore_status is False, optional error
140 error class to log and raise in case of errors. Must be a
141 (sub)type of GitError.
142 @param error_msg: When passed with error_class, used as a friendly
143 error message.
Eric Li6f27d4f2010-09-29 10:55:17 -0700144 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700145 # TODO(pprabhu) Get rid of the ignore_status argument.
146 # Now that we support raising custom errors, we always want to get a
147 # return code from the command execution, instead of an exception.
148 ignore_status = ignore_status or error_class is not None
beepsd9153b52013-01-23 20:52:46 -0800149 cmd = '%s %s' % (self.gen_git_cmd_base(), cmd)
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700150 rv = self._run(cmd, ignore_status=ignore_status)
151 if rv.exit_status != 0 and error_class is not None:
152 logging.error('git command failed: %s: %s',
153 cmd, error_msg if error_msg is not None else '')
154 logging.error(rv.stderr)
155 raise error_class(error_msg if error_msg is not None
156 else rv.stderr)
157
158 return rv
Eric Li6f27d4f2010-09-29 10:55:17 -0700159
160
Prathmesh Prabhuaf1bd7b2018-06-22 14:11:51 -0700161 def clone(self, remote_branch=None, shallow=False):
beepsd9153b52013-01-23 20:52:46 -0800162 """
163 Clones a repo using giturl and repodir.
164
165 Since we're cloning the master repo we don't have a work tree yet,
166 make sure the getter of the gitcmd doesn't think we do by setting
167 work_tree to None.
168
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800169 @param remote_branch: Specify the remote branch to clone. None if to
170 clone master branch.
Prathmesh Prabhuaf1bd7b2018-06-22 14:11:51 -0700171 @param shallow: If True, do a shallow clone.
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800172
beepsd9153b52013-01-23 20:52:46 -0800173 @raises GitCloneError: if cloning the master repo fails.
174 """
175 logging.info('Cloning git repo %s', self.giturl)
176 cmd = 'clone %s %s ' % (self.giturl, self.repodir)
Shuqian Zhaoae2d0782016-11-15 16:58:47 -0800177 if remote_branch:
178 cmd += '-b %s' % remote_branch
Prathmesh Prabhuaf1bd7b2018-06-22 14:11:51 -0700179 if shallow:
180 cmd += '--depth 1'
beepsd9153b52013-01-23 20:52:46 -0800181 abs_work_tree = self.work_tree
182 self.work_tree = None
183 try:
184 rv = self.gitcmd(cmd, True)
185 if rv.exit_status != 0:
186 logging.error(rv.stderr)
187 raise GitCloneError('Failed to clone git url', rv)
188 else:
189 logging.info(rv.stdout)
190 finally:
191 self.work_tree = abs_work_tree
192
193
beepsaae3f1c2013-03-19 15:49:14 -0700194 def pull(self, rebase=False):
beepsd9153b52013-01-23 20:52:46 -0800195 """
196 Pulls into repodir using giturl.
197
beepsaae3f1c2013-03-19 15:49:14 -0700198 @param rebase: If true forces git pull to perform a rebase instead of a
199 merge.
beepsd9153b52013-01-23 20:52:46 -0800200 @raises GitPullError: if pulling from giturl fails.
201 """
202 logging.info('Updating git repo %s', self.giturl)
beepsaae3f1c2013-03-19 15:49:14 -0700203 cmd = 'pull '
204 if rebase:
205 cmd += '--rebase '
206 cmd += self.giturl
207
beepsd9153b52013-01-23 20:52:46 -0800208 rv = self.gitcmd(cmd, True)
209 if rv.exit_status != 0:
210 logging.error(rv.stderr)
211 e_msg = 'Failed to pull git repo data'
212 raise GitPullError(e_msg, rv)
213
214
215 def commit(self, msg='default'):
216 """
217 Commit changes to repo with the supplied commit msg.
218
219 @param msg: A message that goes with the commit.
220 """
Ningning Xia84190b82018-04-16 15:01:40 -0700221 rv = self.gitcmd('commit -a -m \'%s\'' % msg)
beepsd9153b52013-01-23 20:52:46 -0800222 if rv.exit_status != 0:
223 logging.error(rv.stderr)
Allen Li25af06d2016-09-27 12:01:53 -0700224 raise GitCommitError('Unable to commit', rv)
beepsd9153b52013-01-23 20:52:46 -0800225
226
Ningning Xia84190b82018-04-16 15:01:40 -0700227 def upload_cl(self, remote, remote_branch, local_ref='HEAD', draft=False,
228 dryrun=False):
229 """
230 Upload the change.
231
232 @param remote: The git remote to upload the CL.
233 @param remote_branch: The remote branch to upload the CL.
234 @param local_ref: The local ref to upload.
235 @param draft: Whether to upload the CL as a draft.
236 @param dryrun: Whether the upload operation is a dryrun.
Ningning Xiaa043aad2018-04-23 15:07:09 -0700237
238 @return: Git command result stderr.
Ningning Xia84190b82018-04-16 15:01:40 -0700239 """
240 remote_refspec = (('refs/drafts/%s' if draft else 'refs/for/%s') %
241 remote_branch)
Ningning Xiaa043aad2018-04-23 15:07:09 -0700242 return self.push(remote, local_ref, remote_refspec, dryrun=dryrun)
Ningning Xia84190b82018-04-16 15:01:40 -0700243
244
245 def push(self, remote, local_refspec, remote_refspec, dryrun=False):
246 """
247 Push the change.
248
249 @param remote: The git remote to push the CL.
250 @param local_ref: The local ref to push.
251 @param remote_refspec: The remote ref to push to.
252 @param dryrun: Whether the upload operation is a dryrun.
Ningning Xiaa043aad2018-04-23 15:07:09 -0700253
254 @return: Git command result stderr.
Ningning Xia84190b82018-04-16 15:01:40 -0700255 """
256 cmd = 'push %s %s:%s' % (remote, local_refspec, remote_refspec)
257
258 if dryrun:
259 logging.info('Would run push command: %s.', cmd)
260 return
261
262 rv = self.gitcmd(cmd)
263 if rv.exit_status != 0:
264 logging.error(rv.stderr)
265 raise GitPushError('Unable to push', rv)
266
Ningning Xiaa043aad2018-04-23 15:07:09 -0700267 # The CL url is in the result stderr (not stdout)
268 return rv.stderr
269
Ningning Xia84190b82018-04-16 15:01:40 -0700270
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700271 def reset(self, branch_or_sha):
272 """
273 Reset repo to the given branch or git sha.
274
275 @param branch_or_sha: Name of a local or remote branch or git sha.
276
277 @raises GitResetError if operation fails.
278 """
279 self.gitcmd('reset --hard %s' % branch_or_sha,
280 error_class=GitResetError,
281 error_msg='Failed to reset to %s' % branch_or_sha)
282
283
beepsd9153b52013-01-23 20:52:46 -0800284 def reset_head(self):
285 """
286 Reset repo to HEAD@{0} by running git reset --hard HEAD.
287
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700288 TODO(pprabhu): cleanup. Use reset.
289
beepsd9153b52013-01-23 20:52:46 -0800290 @raises GitResetError: if we fails to reset HEAD.
291 """
292 logging.info('Resetting head on repo %s', self.repodir)
293 rv = self.gitcmd('reset --hard HEAD')
294 if rv.exit_status != 0:
295 logging.error(rv.stderr)
296 e_msg = 'Failed to reset HEAD'
297 raise GitResetError(e_msg, rv)
298
299
300 def fetch_remote(self):
301 """
302 Fetches all files from the remote but doesn't reset head.
303
304 @raises GitFetchError: if we fail to fetch all files from giturl.
305 """
306 logging.info('fetching from repo %s', self.giturl)
307 rv = self.gitcmd('fetch --all')
308 if rv.exit_status != 0:
309 logging.error(rv.stderr)
310 e_msg = 'Failed to fetch from %s' % self.giturl
311 raise GitFetchError(e_msg, rv)
312
313
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700314 def reinit_repo_at(self, remote_branch):
beepsd9153b52013-01-23 20:52:46 -0800315 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700316 Does all it can to ensure that the repo is at remote_branch.
317
318 This will try to be nice and detect any local changes and bail early.
319 OTOH, if it finishes successfully, it'll blow away anything and
320 everything so that local repo reflects the upstream branch requested.
Allen Li25af06d2016-09-27 12:01:53 -0700321
322 @param remote_branch: branch to check out.
beepsd9153b52013-01-23 20:52:46 -0800323 """
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700324 if not self.is_repo_initialized():
beepsd9153b52013-01-23 20:52:46 -0800325 self.clone()
326
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700327 # Play nice. Detect any local changes and bail.
328 # Re-stat all files before comparing index. This is needed for
329 # diff-index to work properly in cases when the stat info on files is
330 # stale. (e.g., you just untarred the whole git folder that you got from
331 # Alice)
332 rv = self.gitcmd('update-index --refresh -q',
333 error_class=GitError,
334 error_msg='Failed to refresh index.')
335 rv = self.gitcmd(
336 'diff-index --quiet HEAD --',
337 error_class=GitError,
338 error_msg='Failed to check for local changes.')
339 if rv.stdout:
Allen Li25af06d2016-09-27 12:01:53 -0700340 logging.error(rv.stdout)
Prathmesh Prabhu5f6b2332015-04-10 16:41:28 -0700341 e_msg = 'Local checkout dirty. (%s)'
342 raise GitError(e_msg % rv.stdout)
343
344 # Play the bad cop. Destroy everything in your path.
345 # Don't trust the existing repo setup at all (so don't trust the current
346 # config, current branches / remotes etc).
347 self.gitcmd('config remote.origin.url %s' % self.giturl,
348 error_class=GitError,
349 error_msg='Failed to set origin.')
350 self.gitcmd('checkout -f',
351 error_class=GitError,
352 error_msg='Failed to checkout.')
353 self.gitcmd('clean -qxdf',
354 error_class=GitError,
355 error_msg='Failed to clean.')
356 self.fetch_remote()
357 self.reset('origin/%s' % remote_branch)
358
beepsd9153b52013-01-23 20:52:46 -0800359
Eric Li6f27d4f2010-09-29 10:55:17 -0700360 def get(self, **kwargs):
361 """
362 This method overrides baseclass get so we can do proper git
363 clone/pulls, and check for updated versions. The result of
364 this method will leave an up-to-date version of git repo at
365 'giturl' in 'repodir' directory to be used by build/install
366 methods.
367
beeps98365d82013-02-20 20:08:07 -0800368 @param kwargs: Dictionary of parameters to the method get.
Eric Li6f27d4f2010-09-29 10:55:17 -0700369 """
370 if not self.is_repo_initialized():
371 # this is your first time ...
beepsd9153b52013-01-23 20:52:46 -0800372 self.clone()
373 elif self.is_out_of_date():
Eric Li6f27d4f2010-09-29 10:55:17 -0700374 # exiting repo, check if we're up-to-date
beepsd9153b52013-01-23 20:52:46 -0800375 self.pull()
376 else:
377 logging.info('repo up-to-date')
Eric Li6f27d4f2010-09-29 10:55:17 -0700378
379 # remember where the source is
380 self.source_material = self.repodir
381
382
383 def get_local_head(self):
384 """
385 Get the top commit hash of the current local git branch.
386
387 @return: Top commit hash of local git branch
388 """
389 cmd = 'log --pretty=format:"%H" -1'
390 l_head_cmd = self.gitcmd(cmd)
391 return l_head_cmd.stdout.strip()
392
393
394 def get_remote_head(self):
395 """
396 Get the top commit hash of the current remote git branch.
397
398 @return: Top commit hash of remote git branch
399 """
400 cmd1 = 'remote show'
401 origin_name_cmd = self.gitcmd(cmd1)
402 cmd2 = 'log --pretty=format:"%H" -1 ' + origin_name_cmd.stdout.strip()
403 r_head_cmd = self.gitcmd(cmd2)
404 return r_head_cmd.stdout.strip()
405
406
407 def is_out_of_date(self):
408 """
409 Return whether this branch is out of date with regards to remote branch.
410
411 @return: False, if the branch is outdated, True if it is current.
412 """
413 local_head = self.get_local_head()
414 remote_head = self.get_remote_head()
415
416 # local is out-of-date, pull
417 if local_head != remote_head:
418 return True
419
420 return False
421
422
423 def is_repo_initialized(self):
424 """
beepsd9153b52013-01-23 20:52:46 -0800425 Return whether the git repo was already initialized.
Eric Li6f27d4f2010-09-29 10:55:17 -0700426
beepsd9153b52013-01-23 20:52:46 -0800427 Counts objects in .git directory, since these will exist even if the
428 repo is empty. Assumes non-bare reposotories like the rest of this file.
429
430 @return: True if the repo is initialized.
Eric Li6f27d4f2010-09-29 10:55:17 -0700431 """
beepsd9153b52013-01-23 20:52:46 -0800432 cmd = 'count-objects'
Eric Li6f27d4f2010-09-29 10:55:17 -0700433 rv = self.gitcmd(cmd, True)
434 if rv.exit_status == 0:
435 return True
436
437 return False
438
439
beepsd9153b52013-01-23 20:52:46 -0800440 def get_latest_commit_hash(self):
441 """
442 Get the commit hash of the latest commit in the repo.
443
444 We don't raise an exception if no commit hash was found as
445 this could be an empty repository. The caller should notice this
446 methods return value and raise one appropriately.
447
448 @return: The first commit hash if anything has been committed.
449 """
450 cmd = 'rev-list -n 1 --all'
451 rv = self.gitcmd(cmd, True)
452 if rv.exit_status == 0:
453 return rv.stdout
454 return None
455
456
457 def is_repo_empty(self):
458 """
459 Checks for empty but initialized repos.
460
461 eg: we clone an empty master repo, then don't pull
462 after the master commits.
463
464 @return True if the repo has no commits.
465 """
466 if self.get_latest_commit_hash():
467 return False
468 return True
469
470
Eric Li6f27d4f2010-09-29 10:55:17 -0700471 def get_revision(self):
472 """
473 Return current HEAD commit id
474 """
475 if not self.is_repo_initialized():
476 self.get()
477
478 cmd = 'rev-parse --verify HEAD'
479 gitlog = self.gitcmd(cmd, True)
480 if gitlog.exit_status != 0:
481 logging.error(gitlog.stderr)
482 raise error.CmdError('Failed to find git sha1 revision', gitlog)
483 else:
484 return gitlog.stdout.strip('\n')
485
486
487 def checkout(self, remote, local=None):
488 """
489 Check out the git commit id, branch, or tag given by remote.
490
491 Optional give the local branch name as local.
492
493 @param remote: Remote commit hash
494 @param local: Local commit hash
495 @note: For git checkout tag git version >= 1.5.0 is required
496 """
497 if not self.is_repo_initialized():
498 self.get()
499
500 assert(isinstance(remote, basestring))
501 if local:
502 cmd = 'checkout -b %s %s' % (local, remote)
503 else:
504 cmd = 'checkout %s' % (remote)
505 gitlog = self.gitcmd(cmd, True)
506 if gitlog.exit_status != 0:
507 logging.error(gitlog.stderr)
508 raise error.CmdError('Failed to checkout git branch', gitlog)
509 else:
510 logging.info(gitlog.stdout)
511
512
513 def get_branch(self, all=False, remote_tracking=False):
514 """
515 Show the branches.
516
517 @param all: List both remote-tracking branches and local branches (True)
518 or only the local ones (False).
519 @param remote_tracking: Lists the remote-tracking branches.
520 """
521 if not self.is_repo_initialized():
522 self.get()
523
524 cmd = 'branch --no-color'
525 if all:
526 cmd = " ".join([cmd, "-a"])
527 if remote_tracking:
528 cmd = " ".join([cmd, "-r"])
529
530 gitlog = self.gitcmd(cmd, True)
531 if gitlog.exit_status != 0:
532 logging.error(gitlog.stderr)
533 raise error.CmdError('Failed to get git branch', gitlog)
534 elif all or remote_tracking:
535 return gitlog.stdout.strip('\n')
536 else:
537 branch = [b[2:] for b in gitlog.stdout.split('\n')
538 if b.startswith('*')][0]
539 return branch
Ningning Xia84190b82018-04-16 15:01:40 -0700540
541
542 def status(self, short=True):
543 """
544 Return the current status of the git repo.
545
546 @param short: Whether to give the output in the short-format.
547 """
548 cmd = 'status'
549
550 if short:
551 cmd += ' -s'
552
553 gitlog = self.gitcmd(cmd, True)
554 if gitlog.exit_status != 0:
555 logging.error(gitlog.stderr)
556 raise error.CmdError('Failed to get git status', gitlog)
557 else:
Ningning Xiaef35cb52018-05-04 17:58:20 -0700558 return gitlog.stdout.strip('\n')
559
560
561 def config(self, option_name):
562 """
563 Return the git config value for the given option name.
564
565 @option_name: The name of the git option to get.
566 """
567 cmd = 'config ' + option_name
568 gitlog = self.gitcmd(cmd)
569
570 if gitlog.exit_status != 0:
571 logging.error(gitlog.stderr)
572 raise error.CmdError('Failed to get git config %', option_name)
573 else:
574 return gitlog.stdout.strip('\n')
Ningning Xiaee3e3a92018-05-22 17:38:51 -0700575
576
577 def remote(self):
578 """
579 Return repository git remote name.
580 """
581 gitlog = self.gitcmd('remote')
582
583 if gitlog.exit_status != 0:
584 logging.error(gitlog.stderr)
585 raise error.CmdError('Failed to run git remote.')
586 else:
587 return gitlog.stdout.strip('\n')