1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = ''' 12--- 13module: git 14author: 15 - "Ansible Core Team" 16 - "Michael DeHaan" 17version_added: "0.0.1" 18short_description: Deploy software (or files) from git checkouts 19description: 20 - Manage I(git) checkouts of repositories to deploy files or software. 21options: 22 repo: 23 description: 24 - git, SSH, or HTTP(S) protocol address of the git repository. 25 type: str 26 required: true 27 aliases: [ name ] 28 dest: 29 description: 30 - The path of where the repository should be checked out. This 31 is equivalent to C(git clone [repo_url] [directory]). The repository 32 named in I(repo) is not appended to this path and the destination directory must be empty. This 33 parameter is required, unless I(clone) is set to C(no). 34 type: path 35 required: true 36 version: 37 description: 38 - What version of the repository to check out. This can be 39 the literal string C(HEAD), a branch name, a tag name. 40 It can also be a I(SHA-1) hash, in which case I(refspec) needs 41 to be specified if the given revision is not already available. 42 type: str 43 default: "HEAD" 44 accept_hostkey: 45 description: 46 - If C(yes), ensure that "-o StrictHostKeyChecking=no" is 47 present as an ssh option. 48 type: bool 49 default: 'no' 50 version_added: "1.5" 51 ssh_opts: 52 description: 53 - Creates a wrapper script and exports the path as GIT_SSH 54 which git then automatically uses to override ssh arguments. 55 An example value could be "-o StrictHostKeyChecking=no" 56 (although this particular option is better set by 57 I(accept_hostkey)). 58 type: str 59 version_added: "1.5" 60 key_file: 61 description: 62 - Specify an optional private key file path, on the target host, to use for the checkout. 63 type: path 64 version_added: "1.5" 65 reference: 66 description: 67 - Reference repository (see "git clone --reference ..."). 68 version_added: "1.4" 69 remote: 70 description: 71 - Name of the remote. 72 type: str 73 default: "origin" 74 refspec: 75 description: 76 - Add an additional refspec to be fetched. 77 If version is set to a I(SHA-1) not reachable from any branch 78 or tag, this option may be necessary to specify the ref containing 79 the I(SHA-1). 80 Uses the same syntax as the C(git fetch) command. 81 An example value could be "refs/meta/config". 82 type: str 83 version_added: "1.9" 84 force: 85 description: 86 - If C(yes), any modified files in the working 87 repository will be discarded. Prior to 0.7, this was always 88 'yes' and could not be disabled. Prior to 1.9, the default was 89 `yes`. 90 type: bool 91 default: 'no' 92 version_added: "0.7" 93 depth: 94 description: 95 - Create a shallow clone with a history truncated to the specified 96 number or revisions. The minimum possible value is C(1), otherwise 97 ignored. Needs I(git>=1.9.1) to work correctly. 98 type: int 99 version_added: "1.2" 100 clone: 101 description: 102 - If C(no), do not clone the repository even if it does not exist locally. 103 type: bool 104 default: 'yes' 105 version_added: "1.9" 106 update: 107 description: 108 - If C(no), do not retrieve new revisions from the origin repository. 109 - Operations like archive will work on the existing (old) repository and might 110 not respond to changes to the options version or remote. 111 type: bool 112 default: 'yes' 113 version_added: "1.2" 114 executable: 115 description: 116 - Path to git executable to use. If not supplied, 117 the normal mechanism for resolving binary paths will be used. 118 type: path 119 version_added: "1.4" 120 bare: 121 description: 122 - If C(yes), repository will be created as a bare repo, otherwise 123 it will be a standard repo with a workspace. 124 type: bool 125 default: 'no' 126 version_added: "1.4" 127 umask: 128 description: 129 - The umask to set before doing any checkouts, or any other 130 repository maintenance. 131 type: raw 132 version_added: "2.2" 133 134 recursive: 135 description: 136 - If C(no), repository will be cloned without the --recursive 137 option, skipping sub-modules. 138 type: bool 139 default: 'yes' 140 version_added: "1.6" 141 142 single_branch: 143 description: 144 - Clone only the history leading to the tip of the specified revision. 145 type: bool 146 default: 'no' 147 version_added: '2.11' 148 149 track_submodules: 150 description: 151 - If C(yes), submodules will track the latest commit on their 152 master branch (or other branch specified in .gitmodules). If 153 C(no), submodules will be kept at the revision specified by the 154 main project. This is equivalent to specifying the --remote flag 155 to git submodule update. 156 type: bool 157 default: 'no' 158 version_added: "1.8" 159 160 verify_commit: 161 description: 162 - If C(yes), when cloning or checking out a I(version) verify the 163 signature of a GPG signed commit. This requires git version>=2.1.0 164 to be installed. The commit MUST be signed and the public key MUST 165 be present in the GPG keyring. 166 type: bool 167 default: 'no' 168 version_added: "2.0" 169 170 archive: 171 description: 172 - Specify archive file path with extension. If specified, creates an 173 archive file of the specified format containing the tree structure 174 for the source tree. 175 Allowed archive formats ["zip", "tar.gz", "tar", "tgz"]. 176 - This will clone and perform git archive from local directory as not 177 all git servers support git archive. 178 type: path 179 version_added: "2.4" 180 181 archive_prefix: 182 description: 183 - Specify a prefix to add to each file path in archive. Requires I(archive) to be specified. 184 version_added: "2.10" 185 type: str 186 187 separate_git_dir: 188 description: 189 - The path to place the cloned repository. If specified, Git repository 190 can be separated from working tree. 191 type: path 192 version_added: "2.7" 193 194 gpg_whitelist: 195 description: 196 - A list of trusted GPG fingerprints to compare to the fingerprint of the 197 GPG-signed commit. 198 - Only used when I(verify_commit=yes). 199 - Use of this feature requires Git 2.6+ due to its reliance on git's C(--raw) flag to C(verify-commit) and C(verify-tag). 200 type: list 201 elements: str 202 default: [] 203 version_added: "2.9" 204 205requirements: 206 - git>=1.7.1 (the command line tool) 207 208notes: 209 - "If the task seems to be hanging, first verify remote host is in C(known_hosts). 210 SSH will prompt user to authorize the first contact with a remote host. To avoid this prompt, 211 one solution is to use the option accept_hostkey. Another solution is to 212 add the remote host public key in C(/etc/ssh/ssh_known_hosts) before calling 213 the git module, with the following command: ssh-keyscan -H remote_host.com >> /etc/ssh/ssh_known_hosts." 214 - Supports C(check_mode). 215''' 216 217EXAMPLES = ''' 218- name: Git checkout 219 ansible.builtin.git: 220 repo: 'https://foosball.example.org/path/to/repo.git' 221 dest: /srv/checkout 222 version: release-0.22 223 224- name: Read-write git checkout from github 225 ansible.builtin.git: 226 repo: git@github.com:mylogin/hello.git 227 dest: /home/mylogin/hello 228 229- name: Just ensuring the repo checkout exists 230 ansible.builtin.git: 231 repo: 'https://foosball.example.org/path/to/repo.git' 232 dest: /srv/checkout 233 update: no 234 235- name: Just get information about the repository whether or not it has already been cloned locally 236 ansible.builtin.git: 237 repo: 'https://foosball.example.org/path/to/repo.git' 238 dest: /srv/checkout 239 clone: no 240 update: no 241 242- name: Checkout a github repo and use refspec to fetch all pull requests 243 ansible.builtin.git: 244 repo: https://github.com/ansible/ansible-examples.git 245 dest: /src/ansible-examples 246 refspec: '+refs/pull/*:refs/heads/*' 247 248- name: Create git archive from repo 249 ansible.builtin.git: 250 repo: https://github.com/ansible/ansible-examples.git 251 dest: /src/ansible-examples 252 archive: /tmp/ansible-examples.zip 253 254- name: Clone a repo with separate git directory 255 ansible.builtin.git: 256 repo: https://github.com/ansible/ansible-examples.git 257 dest: /src/ansible-examples 258 separate_git_dir: /src/ansible-examples.git 259 260- name: Example clone of a single branch 261 ansible.builtin.git: 262 repo: https://github.com/ansible/ansible-examples.git 263 dest: /src/ansible-examples 264 single_branch: yes 265 version: master 266 267- name: Avoid hanging when http(s) password is missing 268 ansible.builtin.git: 269 repo: https://github.com/ansible/could-be-a-private-repo 270 dest: /src/from-private-repo 271 environment: 272 GIT_TERMINAL_PROMPT: 0 # reports "terminal prompts disabled" on missing password 273 # or GIT_ASKPASS: /bin/true # for git before version 2.3.0, reports "Authentication failed" on missing password 274''' 275 276RETURN = ''' 277after: 278 description: Last commit revision of the repository retrieved during the update. 279 returned: success 280 type: str 281 sample: 4c020102a9cd6fe908c9a4a326a38f972f63a903 282before: 283 description: Commit revision before the repository was updated, "null" for new repository. 284 returned: success 285 type: str 286 sample: 67c04ebe40a003bda0efb34eacfb93b0cafdf628 287remote_url_changed: 288 description: Contains True or False whether or not the remote URL was changed. 289 returned: success 290 type: bool 291 sample: True 292warnings: 293 description: List of warnings if requested features were not available due to a too old git version. 294 returned: error 295 type: str 296 sample: git version is too old to fully support the depth argument. Falling back to full checkouts. 297git_dir_now: 298 description: Contains the new path of .git directory if it is changed. 299 returned: success 300 type: str 301 sample: /path/to/new/git/dir 302git_dir_before: 303 description: Contains the original path of .git directory if it is changed. 304 returned: success 305 type: str 306 sample: /path/to/old/git/dir 307''' 308 309import filecmp 310import os 311import re 312import shlex 313import stat 314import sys 315import shutil 316import tempfile 317from distutils.version import LooseVersion 318 319from ansible.module_utils.basic import AnsibleModule 320from ansible.module_utils.six import b, string_types 321from ansible.module_utils._text import to_native, to_text 322 323 324def relocate_repo(module, result, repo_dir, old_repo_dir, worktree_dir): 325 if os.path.exists(repo_dir): 326 module.fail_json(msg='Separate-git-dir path %s already exists.' % repo_dir) 327 if worktree_dir: 328 dot_git_file_path = os.path.join(worktree_dir, '.git') 329 try: 330 shutil.move(old_repo_dir, repo_dir) 331 with open(dot_git_file_path, 'w') as dot_git_file: 332 dot_git_file.write('gitdir: %s' % repo_dir) 333 result['git_dir_before'] = old_repo_dir 334 result['git_dir_now'] = repo_dir 335 except (IOError, OSError) as err: 336 # if we already moved the .git dir, roll it back 337 if os.path.exists(repo_dir): 338 shutil.move(repo_dir, old_repo_dir) 339 module.fail_json(msg=u'Unable to move git dir. %s' % to_text(err)) 340 341 342def head_splitter(headfile, remote, module=None, fail_on_error=False): 343 '''Extract the head reference''' 344 # https://github.com/ansible/ansible-modules-core/pull/907 345 346 res = None 347 if os.path.exists(headfile): 348 rawdata = None 349 try: 350 f = open(headfile, 'r') 351 rawdata = f.readline() 352 f.close() 353 except Exception: 354 if fail_on_error and module: 355 module.fail_json(msg="Unable to read %s" % headfile) 356 if rawdata: 357 try: 358 rawdata = rawdata.replace('refs/remotes/%s' % remote, '', 1) 359 refparts = rawdata.split(' ') 360 newref = refparts[-1] 361 nrefparts = newref.split('/', 2) 362 res = nrefparts[-1].rstrip('\n') 363 except Exception: 364 if fail_on_error and module: 365 module.fail_json(msg="Unable to split head from '%s'" % rawdata) 366 return res 367 368 369def unfrackgitpath(path): 370 if path is None: 371 return None 372 373 # copied from ansible.utils.path 374 return os.path.normpath(os.path.realpath(os.path.expanduser(os.path.expandvars(path)))) 375 376 377def get_submodule_update_params(module, git_path, cwd): 378 # or: git submodule [--quiet] update [--init] [-N|--no-fetch] 379 # [-f|--force] [--rebase] [--reference <repository>] [--merge] 380 # [--recursive] [--] [<path>...] 381 382 params = [] 383 384 # run a bad submodule command to get valid params 385 cmd = "%s submodule update --help" % (git_path) 386 rc, stdout, stderr = module.run_command(cmd, cwd=cwd) 387 lines = stderr.split('\n') 388 update_line = None 389 for line in lines: 390 if 'git submodule [--quiet] update ' in line: 391 update_line = line 392 if update_line: 393 update_line = update_line.replace('[', '') 394 update_line = update_line.replace(']', '') 395 update_line = update_line.replace('|', ' ') 396 parts = shlex.split(update_line) 397 for part in parts: 398 if part.startswith('--'): 399 part = part.replace('--', '') 400 params.append(part) 401 402 return params 403 404 405def write_ssh_wrapper(module_tmpdir): 406 try: 407 # make sure we have full permission to the module_dir, which 408 # may not be the case if we're sudo'ing to a non-root user 409 if os.access(module_tmpdir, os.W_OK | os.R_OK | os.X_OK): 410 fd, wrapper_path = tempfile.mkstemp(prefix=module_tmpdir + '/') 411 else: 412 raise OSError 413 except (IOError, OSError): 414 fd, wrapper_path = tempfile.mkstemp() 415 fh = os.fdopen(fd, 'w+b') 416 template = b("""#!/bin/sh 417if [ -z "$GIT_SSH_OPTS" ]; then 418 BASEOPTS="" 419else 420 BASEOPTS=$GIT_SSH_OPTS 421fi 422 423# Let ssh fail rather than prompt 424BASEOPTS="$BASEOPTS -o BatchMode=yes" 425 426if [ -z "$GIT_KEY" ]; then 427 ssh $BASEOPTS "$@" 428else 429 ssh -i "$GIT_KEY" -o IdentitiesOnly=yes $BASEOPTS "$@" 430fi 431""") 432 fh.write(template) 433 fh.close() 434 st = os.stat(wrapper_path) 435 os.chmod(wrapper_path, st.st_mode | stat.S_IEXEC) 436 return wrapper_path 437 438 439def set_git_ssh(ssh_wrapper, key_file, ssh_opts): 440 441 if os.environ.get("GIT_SSH"): 442 del os.environ["GIT_SSH"] 443 os.environ["GIT_SSH"] = ssh_wrapper 444 445 if os.environ.get("GIT_KEY"): 446 del os.environ["GIT_KEY"] 447 448 if key_file: 449 os.environ["GIT_KEY"] = key_file 450 451 if os.environ.get("GIT_SSH_OPTS"): 452 del os.environ["GIT_SSH_OPTS"] 453 454 if ssh_opts: 455 os.environ["GIT_SSH_OPTS"] = ssh_opts 456 457 458def get_version(module, git_path, dest, ref="HEAD"): 459 ''' samples the version of the git repo ''' 460 461 cmd = "%s rev-parse %s" % (git_path, ref) 462 rc, stdout, stderr = module.run_command(cmd, cwd=dest) 463 sha = to_native(stdout).rstrip('\n') 464 return sha 465 466 467def get_submodule_versions(git_path, module, dest, version='HEAD'): 468 cmd = [git_path, 'submodule', 'foreach', git_path, 'rev-parse', version] 469 (rc, out, err) = module.run_command(cmd, cwd=dest) 470 if rc != 0: 471 module.fail_json( 472 msg='Unable to determine hashes of submodules', 473 stdout=out, 474 stderr=err, 475 rc=rc) 476 submodules = {} 477 subm_name = None 478 for line in out.splitlines(): 479 if line.startswith("Entering '"): 480 subm_name = line[10:-1] 481 elif len(line.strip()) == 40: 482 if subm_name is None: 483 module.fail_json() 484 submodules[subm_name] = line.strip() 485 subm_name = None 486 else: 487 module.fail_json(msg='Unable to parse submodule hash line: %s' % line.strip()) 488 if subm_name is not None: 489 module.fail_json(msg='Unable to find hash for submodule: %s' % subm_name) 490 491 return submodules 492 493 494def clone(git_path, module, repo, dest, remote, depth, version, bare, 495 reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch): 496 ''' makes a new git repo if it does not already exist ''' 497 dest_dirname = os.path.dirname(dest) 498 try: 499 os.makedirs(dest_dirname) 500 except Exception: 501 pass 502 cmd = [git_path, 'clone'] 503 504 if bare: 505 cmd.append('--bare') 506 else: 507 cmd.extend(['--origin', remote]) 508 509 is_branch_or_tag = is_remote_branch(git_path, module, dest, repo, version) or is_remote_tag(git_path, module, dest, repo, version) 510 if depth: 511 if version == 'HEAD' or refspec: 512 cmd.extend(['--depth', str(depth)]) 513 elif is_branch_or_tag: 514 cmd.extend(['--depth', str(depth)]) 515 cmd.extend(['--branch', version]) 516 else: 517 # only use depth if the remote object is branch or tag (i.e. fetchable) 518 module.warn("Ignoring depth argument. " 519 "Shallow clones are only available for " 520 "HEAD, branches, tags or in combination with refspec.") 521 if reference: 522 cmd.extend(['--reference', str(reference)]) 523 524 if single_branch: 525 if git_version_used is None: 526 module.fail_json(msg='Cannot find git executable at %s' % git_path) 527 528 if git_version_used < LooseVersion('1.7.10'): 529 module.warn("git version '%s' is too old to use 'single-branch'. Ignoring." % git_version_used) 530 else: 531 cmd.append("--single-branch") 532 533 if is_branch_or_tag: 534 cmd.extend(['--branch', version]) 535 536 needs_separate_git_dir_fallback = False 537 if separate_git_dir: 538 if git_version_used is None: 539 module.fail_json(msg='Cannot find git executable at %s' % git_path) 540 if git_version_used < LooseVersion('1.7.5'): 541 # git before 1.7.5 doesn't have separate-git-dir argument, do fallback 542 needs_separate_git_dir_fallback = True 543 else: 544 cmd.append('--separate-git-dir=%s' % separate_git_dir) 545 546 cmd.extend([repo, dest]) 547 module.run_command(cmd, check_rc=True, cwd=dest_dirname) 548 if needs_separate_git_dir_fallback: 549 relocate_repo(module, result, separate_git_dir, os.path.join(dest, ".git"), dest) 550 551 if bare and remote != 'origin': 552 module.run_command([git_path, 'remote', 'add', remote, repo], check_rc=True, cwd=dest) 553 554 if refspec: 555 cmd = [git_path, 'fetch'] 556 if depth: 557 cmd.extend(['--depth', str(depth)]) 558 cmd.extend([remote, refspec]) 559 module.run_command(cmd, check_rc=True, cwd=dest) 560 561 if verify_commit: 562 verify_commit_sign(git_path, module, dest, version, gpg_whitelist) 563 564 565def has_local_mods(module, git_path, dest, bare): 566 if bare: 567 return False 568 569 cmd = "%s status --porcelain" % (git_path) 570 rc, stdout, stderr = module.run_command(cmd, cwd=dest) 571 lines = stdout.splitlines() 572 lines = list(filter(lambda c: not re.search('^\\?\\?.*$', c), lines)) 573 574 return len(lines) > 0 575 576 577def reset(git_path, module, dest): 578 ''' 579 Resets the index and working tree to HEAD. 580 Discards any changes to tracked files in working 581 tree since that commit. 582 ''' 583 cmd = "%s reset --hard HEAD" % (git_path,) 584 return module.run_command(cmd, check_rc=True, cwd=dest) 585 586 587def get_diff(module, git_path, dest, repo, remote, depth, bare, before, after): 588 ''' Return the difference between 2 versions ''' 589 if before is None: 590 return {'prepared': '>> Newly checked out %s' % after} 591 elif before != after: 592 # Ensure we have the object we are referring to during git diff ! 593 git_version_used = git_version(git_path, module) 594 fetch(git_path, module, repo, dest, after, remote, depth, bare, '', git_version_used) 595 cmd = '%s diff %s %s' % (git_path, before, after) 596 (rc, out, err) = module.run_command(cmd, cwd=dest) 597 if rc == 0 and out: 598 return {'prepared': out} 599 elif rc == 0: 600 return {'prepared': '>> No visual differences between %s and %s' % (before, after)} 601 elif err: 602 return {'prepared': '>> Failed to get proper diff between %s and %s:\n>> %s' % (before, after, err)} 603 else: 604 return {'prepared': '>> Failed to get proper diff between %s and %s' % (before, after)} 605 return {} 606 607 608def get_remote_head(git_path, module, dest, version, remote, bare): 609 cloning = False 610 cwd = None 611 tag = False 612 if remote == module.params['repo']: 613 cloning = True 614 elif remote == 'file://' + os.path.expanduser(module.params['repo']): 615 cloning = True 616 else: 617 cwd = dest 618 if version == 'HEAD': 619 if cloning: 620 # cloning the repo, just get the remote's HEAD version 621 cmd = '%s ls-remote %s -h HEAD' % (git_path, remote) 622 else: 623 head_branch = get_head_branch(git_path, module, dest, remote, bare) 624 cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, head_branch) 625 elif is_remote_branch(git_path, module, dest, remote, version): 626 cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) 627 elif is_remote_tag(git_path, module, dest, remote, version): 628 tag = True 629 cmd = '%s ls-remote %s -t refs/tags/%s*' % (git_path, remote, version) 630 else: 631 # appears to be a sha1. return as-is since it appears 632 # cannot check for a specific sha1 on remote 633 return version 634 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=cwd) 635 if len(out) < 1: 636 module.fail_json(msg="Could not determine remote revision for %s" % version, stdout=out, stderr=err, rc=rc) 637 638 out = to_native(out) 639 640 if tag: 641 # Find the dereferenced tag if this is an annotated tag. 642 for tag in out.split('\n'): 643 if tag.endswith(version + '^{}'): 644 out = tag 645 break 646 elif tag.endswith(version): 647 out = tag 648 649 rev = out.split()[0] 650 return rev 651 652 653def is_remote_tag(git_path, module, dest, remote, version): 654 cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version) 655 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) 656 if to_native(version, errors='surrogate_or_strict') in out: 657 return True 658 else: 659 return False 660 661 662def get_branches(git_path, module, dest): 663 branches = [] 664 cmd = '%s branch --no-color -a' % (git_path,) 665 (rc, out, err) = module.run_command(cmd, cwd=dest) 666 if rc != 0: 667 module.fail_json(msg="Could not determine branch data - received %s" % out, stdout=out, stderr=err) 668 for line in out.split('\n'): 669 if line.strip(): 670 branches.append(line.strip()) 671 return branches 672 673 674def get_annotated_tags(git_path, module, dest): 675 tags = [] 676 cmd = [git_path, 'for-each-ref', 'refs/tags/', '--format', '%(objecttype):%(refname:short)'] 677 (rc, out, err) = module.run_command(cmd, cwd=dest) 678 if rc != 0: 679 module.fail_json(msg="Could not determine tag data - received %s" % out, stdout=out, stderr=err) 680 for line in to_native(out).split('\n'): 681 if line.strip(): 682 tagtype, tagname = line.strip().split(':') 683 if tagtype == 'tag': 684 tags.append(tagname) 685 return tags 686 687 688def is_remote_branch(git_path, module, dest, remote, version): 689 cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) 690 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) 691 if to_native(version, errors='surrogate_or_strict') in out: 692 return True 693 else: 694 return False 695 696 697def is_local_branch(git_path, module, dest, branch): 698 branches = get_branches(git_path, module, dest) 699 lbranch = '%s' % branch 700 if lbranch in branches: 701 return True 702 elif '* %s' % branch in branches: 703 return True 704 else: 705 return False 706 707 708def is_not_a_branch(git_path, module, dest): 709 branches = get_branches(git_path, module, dest) 710 for branch in branches: 711 if branch.startswith('* ') and ('no branch' in branch or 'detached from' in branch or 'detached at' in branch): 712 return True 713 return False 714 715 716def get_repo_path(dest, bare): 717 if bare: 718 repo_path = dest 719 else: 720 repo_path = os.path.join(dest, '.git') 721 # Check if the .git is a file. If it is a file, it means that the repository is in external directory respective to the working copy (e.g. we are in a 722 # submodule structure). 723 if os.path.isfile(repo_path): 724 with open(repo_path, 'r') as gitfile: 725 data = gitfile.read() 726 ref_prefix, gitdir = data.rstrip().split('gitdir: ', 1) 727 if ref_prefix: 728 raise ValueError('.git file has invalid git dir reference format') 729 730 # There is a possibility the .git file to have an absolute path. 731 if os.path.isabs(gitdir): 732 repo_path = gitdir 733 else: 734 repo_path = os.path.join(repo_path.split('.git')[0], gitdir) 735 if not os.path.isdir(repo_path): 736 raise ValueError('%s is not a directory' % repo_path) 737 return repo_path 738 739 740def get_head_branch(git_path, module, dest, remote, bare=False): 741 ''' 742 Determine what branch HEAD is associated with. This is partly 743 taken from lib/ansible/utils/__init__.py. It finds the correct 744 path to .git/HEAD and reads from that file the branch that HEAD is 745 associated with. In the case of a detached HEAD, this will look 746 up the branch in .git/refs/remotes/<remote>/HEAD. 747 ''' 748 try: 749 repo_path = get_repo_path(dest, bare) 750 except (IOError, ValueError) as err: 751 # No repo path found 752 """``.git`` file does not have a valid format for detached Git dir.""" 753 module.fail_json( 754 msg='Current repo does not have a valid reference to a ' 755 'separate Git dir or it refers to the invalid path', 756 details=to_text(err), 757 ) 758 # Read .git/HEAD for the name of the branch. 759 # If we're in a detached HEAD state, look up the branch associated with 760 # the remote HEAD in .git/refs/remotes/<remote>/HEAD 761 headfile = os.path.join(repo_path, "HEAD") 762 if is_not_a_branch(git_path, module, dest): 763 headfile = os.path.join(repo_path, 'refs', 'remotes', remote, 'HEAD') 764 branch = head_splitter(headfile, remote, module=module, fail_on_error=True) 765 return branch 766 767 768def get_remote_url(git_path, module, dest, remote): 769 '''Return URL of remote source for repo.''' 770 command = [git_path, 'ls-remote', '--get-url', remote] 771 (rc, out, err) = module.run_command(command, cwd=dest) 772 if rc != 0: 773 # There was an issue getting remote URL, most likely 774 # command is not available in this version of Git. 775 return None 776 return to_native(out).rstrip('\n') 777 778 779def set_remote_url(git_path, module, repo, dest, remote): 780 ''' updates repo from remote sources ''' 781 # Return if remote URL isn't changing. 782 remote_url = get_remote_url(git_path, module, dest, remote) 783 if remote_url == repo or unfrackgitpath(remote_url) == unfrackgitpath(repo): 784 return False 785 786 command = [git_path, 'remote', 'set-url', remote, repo] 787 (rc, out, err) = module.run_command(command, cwd=dest) 788 if rc != 0: 789 label = "set a new url %s for %s" % (repo, remote) 790 module.fail_json(msg="Failed to %s: %s %s" % (label, out, err)) 791 792 # Return False if remote_url is None to maintain previous behavior 793 # for Git versions prior to 1.7.5 that lack required functionality. 794 return remote_url is not None 795 796 797def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used, force=False): 798 ''' updates repo from remote sources ''' 799 set_remote_url(git_path, module, repo, dest, remote) 800 commands = [] 801 802 fetch_str = 'download remote objects and refs' 803 fetch_cmd = [git_path, 'fetch'] 804 805 refspecs = [] 806 if depth: 807 # try to find the minimal set of refs we need to fetch to get a 808 # successful checkout 809 currenthead = get_head_branch(git_path, module, dest, remote) 810 if refspec: 811 refspecs.append(refspec) 812 elif version == 'HEAD': 813 refspecs.append(currenthead) 814 elif is_remote_branch(git_path, module, dest, repo, version): 815 if currenthead != version: 816 # this workaround is only needed for older git versions 817 # 1.8.3 is broken, 1.9.x works 818 # ensure that remote branch is available as both local and remote ref 819 refspecs.append('+refs/heads/%s:refs/heads/%s' % (version, version)) 820 refspecs.append('+refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version)) 821 elif is_remote_tag(git_path, module, dest, repo, version): 822 refspecs.append('+refs/tags/' + version + ':refs/tags/' + version) 823 if refspecs: 824 # if refspecs is empty, i.e. version is neither heads nor tags 825 # assume it is a version hash 826 # fall back to a full clone, otherwise we might not be able to checkout 827 # version 828 fetch_cmd.extend(['--depth', str(depth)]) 829 830 if not depth or not refspecs: 831 # don't try to be minimalistic but do a full clone 832 # also do this if depth is given, but version is something that can't be fetched directly 833 if bare: 834 refspecs = ['+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*'] 835 else: 836 # ensure all tags are fetched 837 if git_version_used >= LooseVersion('1.9'): 838 fetch_cmd.append('--tags') 839 else: 840 # old git versions have a bug in --tags that prevents updating existing tags 841 commands.append((fetch_str, fetch_cmd + [remote])) 842 refspecs = ['+refs/tags/*:refs/tags/*'] 843 if refspec: 844 refspecs.append(refspec) 845 846 if force: 847 fetch_cmd.append('--force') 848 849 fetch_cmd.extend([remote]) 850 851 commands.append((fetch_str, fetch_cmd + refspecs)) 852 853 for (label, command) in commands: 854 (rc, out, err) = module.run_command(command, cwd=dest) 855 if rc != 0: 856 module.fail_json(msg="Failed to %s: %s %s" % (label, out, err), cmd=command) 857 858 859def submodules_fetch(git_path, module, remote, track_submodules, dest): 860 changed = False 861 862 if not os.path.exists(os.path.join(dest, '.gitmodules')): 863 # no submodules 864 return changed 865 866 gitmodules_file = open(os.path.join(dest, '.gitmodules'), 'r') 867 for line in gitmodules_file: 868 # Check for new submodules 869 if not changed and line.strip().startswith('path'): 870 path = line.split('=', 1)[1].strip() 871 # Check that dest/path/.git exists 872 if not os.path.exists(os.path.join(dest, path, '.git')): 873 changed = True 874 875 # Check for updates to existing modules 876 if not changed: 877 # Fetch updates 878 begin = get_submodule_versions(git_path, module, dest) 879 cmd = [git_path, 'submodule', 'foreach', git_path, 'fetch'] 880 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) 881 if rc != 0: 882 module.fail_json(msg="Failed to fetch submodules: %s" % out + err) 883 884 if track_submodules: 885 # Compare against submodule HEAD 886 # FIXME: determine this from .gitmodules 887 version = 'master' 888 after = get_submodule_versions(git_path, module, dest, '%s/%s' % (remote, version)) 889 if begin != after: 890 changed = True 891 else: 892 # Compare against the superproject's expectation 893 cmd = [git_path, 'submodule', 'status'] 894 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) 895 if rc != 0: 896 module.fail_json(msg='Failed to retrieve submodule status: %s' % out + err) 897 for line in out.splitlines(): 898 if line[0] != ' ': 899 changed = True 900 break 901 return changed 902 903 904def submodule_update(git_path, module, dest, track_submodules, force=False): 905 ''' init and update any submodules ''' 906 907 # get the valid submodule params 908 params = get_submodule_update_params(module, git_path, dest) 909 910 # skip submodule commands if .gitmodules is not present 911 if not os.path.exists(os.path.join(dest, '.gitmodules')): 912 return (0, '', '') 913 cmd = [git_path, 'submodule', 'sync'] 914 (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) 915 if 'remote' in params and track_submodules: 916 cmd = [git_path, 'submodule', 'update', '--init', '--recursive', '--remote'] 917 else: 918 cmd = [git_path, 'submodule', 'update', '--init', '--recursive'] 919 if force: 920 cmd.append('--force') 921 (rc, out, err) = module.run_command(cmd, cwd=dest) 922 if rc != 0: 923 module.fail_json(msg="Failed to init/update submodules: %s" % out + err) 924 return (rc, out, err) 925 926 927def set_remote_branch(git_path, module, dest, remote, version, depth): 928 """set refs for the remote branch version 929 930 This assumes the branch does not yet exist locally and is therefore also not checked out. 931 Can't use git remote set-branches, as it is not available in git 1.7.1 (centos6) 932 """ 933 934 branchref = "+refs/heads/%s:refs/heads/%s" % (version, version) 935 branchref += ' +refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version) 936 cmd = "%s fetch --depth=%s %s %s" % (git_path, depth, remote, branchref) 937 (rc, out, err) = module.run_command(cmd, cwd=dest) 938 if rc != 0: 939 module.fail_json(msg="Failed to fetch branch from remote: %s" % version, stdout=out, stderr=err, rc=rc) 940 941 942def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist): 943 cmd = '' 944 if version == 'HEAD': 945 branch = get_head_branch(git_path, module, dest, remote) 946 (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest) 947 if rc != 0: 948 module.fail_json(msg="Failed to checkout branch %s" % branch, 949 stdout=out, stderr=err, rc=rc) 950 cmd = "%s reset --hard %s/%s --" % (git_path, remote, branch) 951 else: 952 # FIXME check for local_branch first, should have been fetched already 953 if is_remote_branch(git_path, module, dest, remote, version): 954 if depth and not is_local_branch(git_path, module, dest, version): 955 # git clone --depth implies --single-branch, which makes 956 # the checkout fail if the version changes 957 # fetch the remote branch, to be able to check it out next 958 set_remote_branch(git_path, module, dest, remote, version, depth) 959 if not is_local_branch(git_path, module, dest, version): 960 cmd = "%s checkout --track -b %s %s/%s" % (git_path, version, remote, version) 961 else: 962 (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, version), cwd=dest) 963 if rc != 0: 964 module.fail_json(msg="Failed to checkout branch %s" % version, stdout=out, stderr=err, rc=rc) 965 cmd = "%s reset --hard %s/%s" % (git_path, remote, version) 966 else: 967 cmd = "%s checkout --force %s" % (git_path, version) 968 (rc, out1, err1) = module.run_command(cmd, cwd=dest) 969 if rc != 0: 970 if version != 'HEAD': 971 module.fail_json(msg="Failed to checkout %s" % (version), 972 stdout=out1, stderr=err1, rc=rc, cmd=cmd) 973 else: 974 module.fail_json(msg="Failed to checkout branch %s" % (branch), 975 stdout=out1, stderr=err1, rc=rc, cmd=cmd) 976 977 if verify_commit: 978 verify_commit_sign(git_path, module, dest, version, gpg_whitelist) 979 980 return (rc, out1, err1) 981 982 983def verify_commit_sign(git_path, module, dest, version, gpg_whitelist): 984 if version in get_annotated_tags(git_path, module, dest): 985 git_sub = "verify-tag" 986 else: 987 git_sub = "verify-commit" 988 cmd = "%s %s %s" % (git_path, git_sub, version) 989 if gpg_whitelist: 990 cmd += " --raw" 991 (rc, out, err) = module.run_command(cmd, cwd=dest) 992 if rc != 0: 993 module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc) 994 if gpg_whitelist: 995 fingerprint = get_gpg_fingerprint(err) 996 if fingerprint not in gpg_whitelist: 997 module.fail_json(msg='The gpg_whitelist does not include the public key "%s" for this commit' % fingerprint, stdout=out, stderr=err, rc=rc) 998 return (rc, out, err) 999 1000 1001def get_gpg_fingerprint(output): 1002 """Return a fingerprint of the primary key. 1003 1004 Ref: 1005 https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l482 1006 """ 1007 for line in output.splitlines(): 1008 data = line.split() 1009 if data[1] != 'VALIDSIG': 1010 continue 1011 1012 # if signed with a subkey, this contains the primary key fingerprint 1013 data_id = 11 if len(data) == 11 else 2 1014 return data[data_id] 1015 1016 1017def git_version(git_path, module): 1018 """return the installed version of git""" 1019 cmd = "%s --version" % git_path 1020 (rc, out, err) = module.run_command(cmd) 1021 if rc != 0: 1022 # one could fail_json here, but the version info is not that important, 1023 # so let's try to fail only on actual git commands 1024 return None 1025 rematch = re.search('git version (.*)$', to_native(out)) 1026 if not rematch: 1027 return None 1028 return LooseVersion(rematch.groups()[0]) 1029 1030 1031def git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version): 1032 """ Create git archive in given source directory """ 1033 cmd = [git_path, 'archive', '--format', archive_fmt, '--output', archive, version] 1034 if archive_prefix is not None: 1035 cmd.insert(-1, '--prefix') 1036 cmd.insert(-1, archive_prefix) 1037 (rc, out, err) = module.run_command(cmd, cwd=dest) 1038 if rc != 0: 1039 module.fail_json(msg="Failed to perform archive operation", 1040 details="Git archive command failed to create " 1041 "archive %s using %s directory." 1042 "Error: %s" % (archive, dest, err)) 1043 return rc, out, err 1044 1045 1046def create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result): 1047 """ Helper function for creating archive using git_archive """ 1048 all_archive_fmt = {'.zip': 'zip', '.gz': 'tar.gz', '.tar': 'tar', 1049 '.tgz': 'tgz'} 1050 _, archive_ext = os.path.splitext(archive) 1051 archive_fmt = all_archive_fmt.get(archive_ext, None) 1052 if archive_fmt is None: 1053 module.fail_json(msg="Unable to get file extension from " 1054 "archive file name : %s" % archive, 1055 details="Please specify archive as filename with " 1056 "extension. File extension can be one " 1057 "of ['tar', 'tar.gz', 'zip', 'tgz']") 1058 1059 repo_name = repo.split("/")[-1].replace(".git", "") 1060 1061 if os.path.exists(archive): 1062 # If git archive file exists, then compare it with new git archive file. 1063 # if match, do nothing 1064 # if does not match, then replace existing with temp archive file. 1065 tempdir = tempfile.mkdtemp() 1066 new_archive_dest = os.path.join(tempdir, repo_name) 1067 new_archive = new_archive_dest + '.' + archive_fmt 1068 git_archive(git_path, module, dest, new_archive, archive_fmt, archive_prefix, version) 1069 1070 # filecmp is supposed to be efficient than md5sum checksum 1071 if filecmp.cmp(new_archive, archive): 1072 result.update(changed=False) 1073 # Cleanup before exiting 1074 try: 1075 shutil.rmtree(tempdir) 1076 except OSError: 1077 pass 1078 else: 1079 try: 1080 shutil.move(new_archive, archive) 1081 shutil.rmtree(tempdir) 1082 result.update(changed=True) 1083 except OSError as e: 1084 module.fail_json(msg="Failed to move %s to %s" % 1085 (new_archive, archive), 1086 details=u"Error occurred while moving : %s" 1087 % to_text(e)) 1088 else: 1089 # Perform archive from local directory 1090 git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version) 1091 result.update(changed=True) 1092 1093 1094# =========================================== 1095 1096def main(): 1097 module = AnsibleModule( 1098 argument_spec=dict( 1099 dest=dict(type='path'), 1100 repo=dict(required=True, aliases=['name']), 1101 version=dict(default='HEAD'), 1102 remote=dict(default='origin'), 1103 refspec=dict(default=None), 1104 reference=dict(default=None), 1105 force=dict(default='no', type='bool'), 1106 depth=dict(default=None, type='int'), 1107 clone=dict(default='yes', type='bool'), 1108 update=dict(default='yes', type='bool'), 1109 verify_commit=dict(default='no', type='bool'), 1110 gpg_whitelist=dict(default=[], type='list', elements='str'), 1111 accept_hostkey=dict(default='no', type='bool'), 1112 key_file=dict(default=None, type='path', required=False), 1113 ssh_opts=dict(default=None, required=False), 1114 executable=dict(default=None, type='path'), 1115 bare=dict(default='no', type='bool'), 1116 recursive=dict(default='yes', type='bool'), 1117 single_branch=dict(default=False, type='bool'), 1118 track_submodules=dict(default='no', type='bool'), 1119 umask=dict(default=None, type='raw'), 1120 archive=dict(type='path'), 1121 archive_prefix=dict(), 1122 separate_git_dir=dict(type='path'), 1123 ), 1124 mutually_exclusive=[('separate_git_dir', 'bare')], 1125 required_by={'archive_prefix': ['archive']}, 1126 supports_check_mode=True 1127 ) 1128 1129 dest = module.params['dest'] 1130 repo = module.params['repo'] 1131 version = module.params['version'] 1132 remote = module.params['remote'] 1133 refspec = module.params['refspec'] 1134 force = module.params['force'] 1135 depth = module.params['depth'] 1136 update = module.params['update'] 1137 allow_clone = module.params['clone'] 1138 bare = module.params['bare'] 1139 verify_commit = module.params['verify_commit'] 1140 gpg_whitelist = module.params['gpg_whitelist'] 1141 reference = module.params['reference'] 1142 single_branch = module.params['single_branch'] 1143 git_path = module.params['executable'] or module.get_bin_path('git', True) 1144 key_file = module.params['key_file'] 1145 ssh_opts = module.params['ssh_opts'] 1146 umask = module.params['umask'] 1147 archive = module.params['archive'] 1148 archive_prefix = module.params['archive_prefix'] 1149 separate_git_dir = module.params['separate_git_dir'] 1150 1151 result = dict(changed=False, warnings=list()) 1152 1153 if module.params['accept_hostkey']: 1154 if ssh_opts is not None: 1155 if "-o StrictHostKeyChecking=no" not in ssh_opts: 1156 ssh_opts += " -o StrictHostKeyChecking=no" 1157 else: 1158 ssh_opts = "-o StrictHostKeyChecking=no" 1159 1160 # evaluate and set the umask before doing anything else 1161 if umask is not None: 1162 if not isinstance(umask, string_types): 1163 module.fail_json(msg="umask must be defined as a quoted octal integer") 1164 try: 1165 umask = int(umask, 8) 1166 except Exception: 1167 module.fail_json(msg="umask must be an octal integer", 1168 details=str(sys.exc_info()[1])) 1169 os.umask(umask) 1170 1171 # Certain features such as depth require a file:/// protocol for path based urls 1172 # so force a protocol here ... 1173 if os.path.expanduser(repo).startswith('/'): 1174 repo = 'file://' + os.path.expanduser(repo) 1175 1176 # We screenscrape a huge amount of git commands so use C locale anytime we 1177 # call run_command() 1178 module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') 1179 1180 if separate_git_dir: 1181 separate_git_dir = os.path.realpath(separate_git_dir) 1182 1183 gitconfig = None 1184 if not dest and allow_clone: 1185 module.fail_json(msg="the destination directory must be specified unless clone=no") 1186 elif dest: 1187 dest = os.path.abspath(dest) 1188 try: 1189 repo_path = get_repo_path(dest, bare) 1190 if separate_git_dir and os.path.exists(repo_path) and separate_git_dir != repo_path: 1191 result['changed'] = True 1192 if not module.check_mode: 1193 relocate_repo(module, result, separate_git_dir, repo_path, dest) 1194 repo_path = separate_git_dir 1195 except (IOError, ValueError) as err: 1196 # No repo path found 1197 """``.git`` file does not have a valid format for detached Git dir.""" 1198 module.fail_json( 1199 msg='Current repo does not have a valid reference to a ' 1200 'separate Git dir or it refers to the invalid path', 1201 details=to_text(err), 1202 ) 1203 gitconfig = os.path.join(repo_path, 'config') 1204 1205 # create a wrapper script and export 1206 # GIT_SSH=<path> as an environment variable 1207 # for git to use the wrapper script 1208 ssh_wrapper = write_ssh_wrapper(module.tmpdir) 1209 set_git_ssh(ssh_wrapper, key_file, ssh_opts) 1210 module.add_cleanup_file(path=ssh_wrapper) 1211 1212 git_version_used = git_version(git_path, module) 1213 1214 if depth is not None and git_version_used < LooseVersion('1.9.1'): 1215 module.warn("git version is too old to fully support the depth argument. Falling back to full checkouts.") 1216 depth = None 1217 1218 recursive = module.params['recursive'] 1219 track_submodules = module.params['track_submodules'] 1220 1221 result.update(before=None) 1222 1223 local_mods = False 1224 if (dest and not os.path.exists(gitconfig)) or (not dest and not allow_clone): 1225 # if there is no git configuration, do a clone operation unless: 1226 # * the user requested no clone (they just want info) 1227 # * we're doing a check mode test 1228 # In those cases we do an ls-remote 1229 if module.check_mode or not allow_clone: 1230 remote_head = get_remote_head(git_path, module, dest, version, repo, bare) 1231 result.update(changed=True, after=remote_head) 1232 if module._diff: 1233 diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after']) 1234 if diff: 1235 result['diff'] = diff 1236 module.exit_json(**result) 1237 # there's no git config, so clone 1238 clone(git_path, module, repo, dest, remote, depth, version, bare, reference, 1239 refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch) 1240 elif not update: 1241 # Just return having found a repo already in the dest path 1242 # this does no checking that the repo is the actual repo 1243 # requested. 1244 result['before'] = get_version(module, git_path, dest) 1245 result.update(after=result['before']) 1246 if archive: 1247 # Git archive is not supported by all git servers, so 1248 # we will first clone and perform git archive from local directory 1249 if module.check_mode: 1250 result.update(changed=True) 1251 module.exit_json(**result) 1252 1253 create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result) 1254 1255 module.exit_json(**result) 1256 else: 1257 # else do a pull 1258 local_mods = has_local_mods(module, git_path, dest, bare) 1259 result['before'] = get_version(module, git_path, dest) 1260 if local_mods: 1261 # failure should happen regardless of check mode 1262 if not force: 1263 module.fail_json(msg="Local modifications exist in repository (force=no).", **result) 1264 # if force and in non-check mode, do a reset 1265 if not module.check_mode: 1266 reset(git_path, module, dest) 1267 result.update(changed=True, msg='Local modifications exist.') 1268 1269 # exit if already at desired sha version 1270 if module.check_mode: 1271 remote_url = get_remote_url(git_path, module, dest, remote) 1272 remote_url_changed = remote_url and remote_url != repo and unfrackgitpath(remote_url) != unfrackgitpath(repo) 1273 else: 1274 remote_url_changed = set_remote_url(git_path, module, repo, dest, remote) 1275 result.update(remote_url_changed=remote_url_changed) 1276 1277 if module.check_mode: 1278 remote_head = get_remote_head(git_path, module, dest, version, remote, bare) 1279 result.update(changed=(result['before'] != remote_head or remote_url_changed), after=remote_head) 1280 # FIXME: This diff should fail since the new remote_head is not fetched yet?! 1281 if module._diff: 1282 diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after']) 1283 if diff: 1284 result['diff'] = diff 1285 module.exit_json(**result) 1286 else: 1287 fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used, force=force) 1288 1289 result['after'] = get_version(module, git_path, dest) 1290 1291 # switch to version specified regardless of whether 1292 # we got new revisions from the repository 1293 if not bare: 1294 switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist) 1295 1296 # Deal with submodules 1297 submodules_updated = False 1298 if recursive and not bare: 1299 submodules_updated = submodules_fetch(git_path, module, remote, track_submodules, dest) 1300 if submodules_updated: 1301 result.update(submodules_changed=submodules_updated) 1302 1303 if module.check_mode: 1304 result.update(changed=True, after=remote_head) 1305 module.exit_json(**result) 1306 1307 # Switch to version specified 1308 submodule_update(git_path, module, dest, track_submodules, force=force) 1309 1310 # determine if we changed anything 1311 result['after'] = get_version(module, git_path, dest) 1312 1313 if result['before'] != result['after'] or local_mods or submodules_updated or remote_url_changed: 1314 result.update(changed=True) 1315 if module._diff: 1316 diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after']) 1317 if diff: 1318 result['diff'] = diff 1319 1320 if archive: 1321 # Git archive is not supported by all git servers, so 1322 # we will first clone and perform git archive from local directory 1323 if module.check_mode: 1324 result.update(changed=True) 1325 module.exit_json(**result) 1326 1327 create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result) 1328 1329 module.exit_json(**result) 1330 1331 1332if __name__ == '__main__': 1333 main() 1334