1# Copyright (C) 2002-2005 Stephen Kennedy <stevek@gnome.org>
2
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions
5# are met:
6#
7# 1. Redistributions of source code must retain the above copyright
8#    notice, this list of conditions and the following disclaimer.
9# 2. Redistributions in binary form must reproduce the above copyright
10#    notice, this list of conditions and the following disclaimer in the
11#    documentation and/or other materials provided with the distribution.
12
13# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
14# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
24import errno
25import os
26import shutil
27import subprocess
28import tempfile
29
30from . import _vc
31
32
33class Vc(_vc.CachedVc):
34
35    CMD = "hg"
36    NAME = "Mercurial"
37    VC_DIR = ".hg"
38
39    state_map = {
40        "?": _vc.STATE_NONE,
41        "A": _vc.STATE_NEW,
42        "C": _vc.STATE_NORMAL,
43        "!": _vc.STATE_MISSING,
44        "I": _vc.STATE_IGNORED,
45        "M": _vc.STATE_MODIFIED,
46        "R": _vc.STATE_REMOVED,
47    }
48
49    def commit_command(self, message):
50        return [self.CMD, "commit", "-m", message]
51
52    def update_command(self):
53        return [self.CMD, "update"]
54
55    def add_command(self):
56        return [self.CMD, "add"]
57
58    def remove_command(self, force=0):
59        return [self.CMD, "rm"]
60
61    def revert_command(self):
62        return [self.CMD, "revert"]
63
64    def valid_repo(self):
65        if _vc.call([self.CMD, "root"], cwd=self.root):
66            return False
67        else:
68            return True
69
70    def get_working_directory(self, workdir):
71        if workdir.startswith("/"):
72            return self.root
73        else:
74            return ''
75
76    def get_path_for_repo_file(self, path, commit=None):
77        if commit is not None:
78            raise NotImplementedError()
79
80        if not path.startswith(self.root + os.path.sep):
81            raise _vc.InvalidVCPath(self, path, "Path not in repository")
82        path = path[len(self.root) + 1:]
83
84        process = subprocess.Popen([self.CMD, "cat", path], cwd=self.root,
85                                   stdout=subprocess.PIPE,
86                                   stderr=subprocess.PIPE)
87
88        with tempfile.NamedTemporaryFile(prefix='meld-tmp', delete=False) as f:
89            shutil.copyfileobj(process.stdout, f)
90        return f.name
91
92    def _update_tree_state_cache(self, path, tree_state):
93        """ Update the state of the file(s) at tree_state['path'] """
94        while 1:
95            try:
96                # Get the status of modified files
97                proc = _vc.popen([self.CMD, "status", '-A', path],
98                                 cwd=self.location)
99                entries = proc.read().split("\n")[:-1]
100
101                # The following command removes duplicate file entries.
102                # Just in case.
103                entries = list(set(entries))
104                break
105            except OSError as e:
106                if e.errno != errno.EAGAIN:
107                    raise
108
109        if len(entries) == 0 and os.path.isfile(path):
110            # If we're just updating a single file there's a chance that it
111            # was it was previously modified, and now has been edited
112            # so that it is un-modified.  This will result in an empty
113            # 'entries' list, and tree_state['path'] will still contain stale
114            # data.  When this corner case occurs we force tree_state['path']
115            # to STATE_NORMAL.
116            tree_state[path] = _vc.STATE_NORMAL
117        else:
118            # There are 1 or more modified files, parse their state
119            for entry in entries:
120                # we might have a space in file name, it should be ignored
121                statekey, name = entry.split(" ", 1)
122                path = os.path.join(self.location, name.strip())
123                state = self.state_map.get(statekey.strip(), _vc.STATE_NONE)
124                tree_state[path] = state
125
126    def _lookup_tree_cache(self, rootdir):
127        # Get a list of all files in rootdir, as well as their status
128        tree_state = {}
129        self._update_tree_state_cache("./", tree_state)
130
131        return tree_state
132
133    def update_file_state(self, path):
134        tree_state = self._get_tree_cache(os.path.dirname(path))
135        self._update_tree_state_cache(path, tree_state)
136
137    def _get_dirsandfiles(self, directory, dirs, files):
138
139        tree = self._get_tree_cache(directory)
140
141        retfiles = []
142        retdirs = []
143        for name, path in files:
144            state = tree.get(path, _vc.STATE_NORMAL)
145            retfiles.append(_vc.File(path, name, state))
146        for name, path in dirs:
147            # mercurial does not operate on dirs, just files
148            retdirs.append(_vc.Dir(path, name, _vc.STATE_NORMAL))
149        for path, state in tree.items():
150            # removed files are not in the filesystem, so must be added here
151            if state in (_vc.STATE_REMOVED, _vc.STATE_MISSING):
152                folder, name = os.path.split(path)
153                if folder == directory:
154                    retfiles.append(_vc.File(path, name, state))
155        return retdirs, retfiles
156