Home | History | Annotate | Download | only in helper
      1 # Copyright 2011 Google Inc. All Rights Reserved.
      2 
      3 __author__ = 'kbaclawski (at] google.com (Krystian Baclawski)'
      4 
      5 import collections
      6 import os.path
      7 
      8 from automation.common import command as cmd
      9 
     10 
     11 class 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 
     81 class 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 
    143 class 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