1# Copyright 2011 Google Inc. All Rights Reserved. 2 3__author__ = 'kbaclawski@google.com (Krystian Baclawski)' 4 5import collections 6import os.path 7 8from automation.common import command as cmd 9 10 11class PathMapping(object): 12 """Stores information about relative path mapping (remote to local).""" 13 14 @classmethod 15 def ListFromPathDict(cls, prefix_path_dict): 16 """Takes {'prefix1': ['path1',...], ...} and returns a list of mappings.""" 17 18 mappings = [] 19 20 for prefix, paths in sorted(prefix_path_dict.items()): 21 for path in sorted(paths): 22 mappings.append(cls(os.path.join(prefix, path))) 23 24 return mappings 25 26 @classmethod 27 def ListFromPathTuples(cls, tuple_list): 28 """Takes a list of tuples and returns a list of mappings. 29 30 Args: 31 tuple_list: [('remote_path1', 'local_path1'), ...] 32 33 Returns: 34 a list of mapping objects 35 """ 36 mappings = [] 37 for remote_path, local_path in tuple_list: 38 mappings.append(cls(remote_path, local_path)) 39 40 return mappings 41 42 def __init__(self, remote, local=None, common_suffix=None): 43 suffix = self._FixPath(common_suffix or '') 44 45 self.remote = os.path.join(remote, suffix) 46 self.local = os.path.join(local or remote, suffix) 47 48 @staticmethod 49 def _FixPath(path_s): 50 parts = [part for part in path_s.strip('/').split('/') if part] 51 52 if not parts: 53 return '' 54 55 return os.path.join(*parts) 56 57 def _GetRemote(self): 58 return self._remote 59 60 def _SetRemote(self, path_s): 61 self._remote = self._FixPath(path_s) 62 63 remote = property(_GetRemote, _SetRemote) 64 65 def _GetLocal(self): 66 return self._local 67 68 def _SetLocal(self, path_s): 69 self._local = self._FixPath(path_s) 70 71 local = property(_GetLocal, _SetLocal) 72 73 def GetAbsolute(self, depot, client): 74 return (os.path.join('//', depot, self.remote), 75 os.path.join('//', client, self.local)) 76 77 def __str__(self): 78 return '%s(%s => %s)' % (self.__class__.__name__, self.remote, self.local) 79 80 81class View(collections.MutableSet): 82 """Keeps all information about local client required to work with perforce.""" 83 84 def __init__(self, depot, mappings=None, client=None): 85 self.depot = depot 86 87 if client: 88 self.client = client 89 90 self._mappings = set(mappings or []) 91 92 @staticmethod 93 def _FixRoot(root_s): 94 parts = root_s.strip('/').split('/', 1) 95 96 if len(parts) != 1: 97 return None 98 99 return parts[0] 100 101 def _GetDepot(self): 102 return self._depot 103 104 def _SetDepot(self, depot_s): 105 depot = self._FixRoot(depot_s) 106 assert depot, 'Not a valid depot name: "%s".' % depot_s 107 self._depot = depot 108 109 depot = property(_GetDepot, _SetDepot) 110 111 def _GetClient(self): 112 return self._client 113 114 def _SetClient(self, client_s): 115 client = self._FixRoot(client_s) 116 assert client, 'Not a valid client name: "%s".' % client_s 117 self._client = client 118 119 client = property(_GetClient, _SetClient) 120 121 def add(self, mapping): 122 assert type(mapping) is PathMapping 123 self._mappings.add(mapping) 124 125 def discard(self, mapping): 126 assert type(mapping) is PathMapping 127 self._mappings.discard(mapping) 128 129 def __contains__(self, value): 130 return value in self._mappings 131 132 def __len__(self): 133 return len(self._mappings) 134 135 def __iter__(self): 136 return iter(mapping for mapping in self._mappings) 137 138 def AbsoluteMappings(self): 139 return iter(mapping.GetAbsolute(self.depot, self.client) 140 for mapping in self._mappings) 141 142 143class CommandsFactory(object): 144 """Creates shell commands used for interaction with Perforce.""" 145 146 def __init__(self, checkout_dir, p4view, name=None, port=None): 147 self.port = port or 'perforce2:2666' 148 self.view = p4view 149 self.view.client = name or 'p4-automation-$HOSTNAME-$JOB_ID' 150 self.checkout_dir = checkout_dir 151 self.p4config_path = os.path.join(self.checkout_dir, '.p4config') 152 153 def Initialize(self): 154 return cmd.Chain('mkdir -p %s' % self.checkout_dir, 'cp ~/.p4config %s' % 155 self.checkout_dir, 'chmod u+w %s' % self.p4config_path, 156 'echo "P4PORT=%s" >> %s' % (self.port, self.p4config_path), 157 'echo "P4CLIENT=%s" >> %s' % 158 (self.view.client, self.p4config_path)) 159 160 def Create(self): 161 # TODO(kbaclawski): Could we support value list for options consistently? 162 mappings = ['-a \"%s %s\"' % mapping 163 for mapping in self.view.AbsoluteMappings()] 164 165 # First command will create client with default mappings. Second one will 166 # replace default mapping with desired. Unfortunately, it seems that it 167 # cannot be done in one step. P4EDITOR is defined to /bin/true because we 168 # don't want "g4 client" to enter real editor and wait for user actions. 169 return cmd.Wrapper( 170 cmd.Chain( 171 cmd.Shell('g4', 'client'), 172 cmd.Shell('g4', 'client', '--replace', *mappings)), 173 env={'P4EDITOR': '/bin/true'}) 174 175 def SaveSpecification(self, filename=None): 176 return cmd.Pipe(cmd.Shell('g4', 'client', '-o'), output=filename) 177 178 def Sync(self, revision=None): 179 sync_arg = '...' 180 if revision: 181 sync_arg = '%s@%s' % (sync_arg, revision) 182 return cmd.Shell('g4', 'sync', sync_arg) 183 184 def SaveCurrentCLNumber(self, filename=None): 185 return cmd.Pipe( 186 cmd.Shell('g4', 'changes', '-m1', '...#have'), 187 cmd.Shell('sed', '-E', '"s,Change ([0-9]+) .*,\\1,"'), 188 output=filename) 189 190 def Remove(self): 191 return cmd.Shell('g4', 'client', '-d', self.view.client) 192 193 def SetupAndDo(self, *commands): 194 return cmd.Chain(self.Initialize(), 195 self.InCheckoutDir(self.Create(), *commands)) 196 197 def InCheckoutDir(self, *commands): 198 return cmd.Wrapper(cmd.Chain(*commands), cwd=self.checkout_dir) 199 200 def CheckoutFromSnapshot(self, snapshot): 201 cmds = cmd.Chain() 202 203 for mapping in self.view: 204 local_path, file_part = mapping.local.rsplit('/', 1) 205 206 if file_part == '...': 207 remote_dir = os.path.join(snapshot, local_path) 208 local_dir = os.path.join(self.checkout_dir, os.path.dirname(local_path)) 209 210 cmds.extend([ 211 cmd.Shell('mkdir', '-p', local_dir), cmd.Shell( 212 'rsync', '-lr', remote_dir, local_dir) 213 ]) 214 215 return cmds 216