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