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