1""" 2Interaction with Mercurial repositories 3======================================= 4 5Before using hg over ssh, make sure the remote host fingerprint already exists 6in ~/.ssh/known_hosts, and the remote host has this host's public key. 7 8.. code-block:: yaml 9 10 https://bitbucket.org/example_user/example_repo: 11 hg.latest: 12 - rev: tip 13 - target: /tmp/example_repo 14""" 15 16 17import logging 18import os 19import shutil 20 21import salt.utils.platform 22from salt.exceptions import CommandExecutionError 23from salt.states.git import _fail, _neutral_test 24 25log = logging.getLogger(__name__) 26 27HG_BINARY = "hg.exe" if salt.utils.platform.is_windows() else "hg" 28 29 30def __virtual__(): 31 """ 32 Only load if hg is available 33 """ 34 if __salt__["cmd.has_exec"](HG_BINARY): 35 return True 36 return (False, "Command {} not found".format(HG_BINARY)) 37 38 39def latest( 40 name, 41 rev=None, 42 target=None, 43 clean=False, 44 user=None, 45 identity=None, 46 force=False, 47 opts=False, 48 update_head=True, 49): 50 """ 51 Make sure the repository is cloned to the given directory and is up to date 52 53 name 54 Address of the remote repository as passed to "hg clone" 55 56 rev 57 The remote branch, tag, or revision hash to clone/pull 58 59 target 60 Target destination directory path on minion to clone into 61 62 clean 63 Force a clean update with -C (Default: False) 64 65 user 66 Name of the user performing repository management operations 67 68 .. versionadded:: 0.17.0 69 70 identity 71 Private SSH key on the minion server for authentication (ssh://) 72 73 .. versionadded:: 2015.5.0 74 75 force 76 Force hg to clone into pre-existing directories (deletes contents) 77 78 opts 79 Include additional arguments and options to the hg command line 80 81 update_head 82 Should we update the head if new changes are found? Defaults to True 83 84 .. versionadded:: 2017.7.0 85 86 """ 87 ret = {"name": name, "result": True, "comment": "", "changes": {}} 88 89 if not target: 90 return _fail(ret, '"target option is required') 91 92 is_repository = os.path.isdir(target) and os.path.isdir("{}/.hg".format(target)) 93 94 if is_repository: 95 ret = _update_repo( 96 ret, name, target, clean, user, identity, rev, opts, update_head 97 ) 98 else: 99 if os.path.isdir(target): 100 fail = _handle_existing(ret, target, force) 101 if fail is not None: 102 return fail 103 else: 104 log.debug('target %s is not found, "hg clone" is required', target) 105 if __opts__["test"]: 106 return _neutral_test( 107 ret, "Repository {} is about to be cloned to {}".format(name, target) 108 ) 109 _clone_repo(ret, target, name, user, identity, rev, opts) 110 return ret 111 112 113def _update_repo(ret, name, target, clean, user, identity, rev, opts, update_head): 114 """ 115 Update the repo to a given revision. Using clean passes -C to the hg up 116 """ 117 log.debug('target %s is found, "hg pull && hg up is probably required"', target) 118 119 current_rev = __salt__["hg.revision"](target, user=user, rev=".") 120 if not current_rev: 121 return _fail(ret, "Seems that {} is not a valid hg repo".format(target)) 122 123 if __opts__["test"]: 124 return _neutral_test( 125 ret, 126 "Repository {} update is probably required (current revision is {})".format( 127 target, current_rev 128 ), 129 ) 130 131 try: 132 pull_out = __salt__["hg.pull"]( 133 target, user=user, identity=identity, opts=opts, repository=name 134 ) 135 except CommandExecutionError as err: 136 ret["result"] = False 137 ret["comment"] = err 138 return ret 139 140 if update_head is False: 141 changes = "no changes found" not in pull_out 142 if changes: 143 ret["comment"] = ( 144 "Update is probably required but update_head=False so we will skip" 145 " updating." 146 ) 147 else: 148 ret[ 149 "comment" 150 ] = "No changes found and update_head=False so will skip updating." 151 return ret 152 153 if rev: 154 try: 155 __salt__["hg.update"](target, rev, force=clean, user=user) 156 except CommandExecutionError as err: 157 ret["result"] = False 158 ret["comment"] = err 159 return ret 160 else: 161 try: 162 __salt__["hg.update"](target, "tip", force=clean, user=user) 163 except CommandExecutionError as err: 164 ret["result"] = False 165 ret["comment"] = err 166 return ret 167 168 new_rev = __salt__["hg.revision"](cwd=target, user=user, rev=".") 169 170 if current_rev != new_rev: 171 revision_text = "{} => {}".format(current_rev, new_rev) 172 log.info("Repository %s updated: %s", target, revision_text) 173 ret["comment"] = "Repository {} updated.".format(target) 174 ret["changes"]["revision"] = revision_text 175 elif "error:" in pull_out: 176 return _fail(ret, "An error was thrown by hg:\n{}".format(pull_out)) 177 return ret 178 179 180def _handle_existing(ret, target, force): 181 not_empty = os.listdir(target) 182 if not not_empty: 183 log.debug( 184 "target %s found, but directory is empty, automatically deleting", target 185 ) 186 shutil.rmtree(target) 187 elif force: 188 log.debug( 189 "target %s found and is not empty. " 190 "Since force option is in use, deleting anyway.", 191 target, 192 ) 193 shutil.rmtree(target) 194 else: 195 return _fail(ret, "Directory exists, and is not empty") 196 197 198def _clone_repo(ret, target, name, user, identity, rev, opts): 199 try: 200 result = __salt__["hg.clone"]( 201 target, name, user=user, identity=identity, opts=opts 202 ) 203 except CommandExecutionError as err: 204 ret["result"] = False 205 ret["comment"] = err 206 return ret 207 208 if not os.path.isdir(target): 209 return _fail(ret, result) 210 211 if rev: 212 try: 213 __salt__["hg.update"](target, rev, user=user) 214 except CommandExecutionError as err: 215 ret["result"] = False 216 ret["comment"] = err 217 return ret 218 219 new_rev = __salt__["hg.revision"](cwd=target, user=user) 220 message = "Repository {} cloned to {}".format(name, target) 221 log.info(message) 222 ret["comment"] = message 223 224 ret["changes"]["new"] = name 225 ret["changes"]["revision"] = new_rev 226 227 return ret 228