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