• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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