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