aboutsummaryrefslogtreecommitdiff
path: root/automation/clients/helper/perforce.py
blob: 1f2dfe791da3fb39965d1a6e8e8b51d12e205b88 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# Copyright 2011 Google Inc. All Rights Reserved.

__author__ = 'kbaclawski@google.com (Krystian Baclawski)'

import collections
import os.path

from automation.common import command as cmd


class PathMapping(object):
  """Stores information about relative path mapping (remote to local)."""

  @classmethod
  def ListFromPathDict(cls, prefix_path_dict):
    """Takes {'prefix1': ['path1',...], ...} and returns a list of mappings."""

    mappings = []

    for prefix, paths in sorted(prefix_path_dict.items()):
      for path in sorted(paths):
        mappings.append(cls(os.path.join(prefix, path)))

    return mappings

  @classmethod
  def ListFromPathTuples(cls, tuple_list):
    """Takes a list of tuples and returns a list of mappings.

    Args:
      tuple_list: [('remote_path1', 'local_path1'), ...]

    Returns:
      a list of mapping objects
    """
    mappings = []
    for remote_path, local_path in tuple_list:
      mappings.append(cls(remote_path, local_path))

    return mappings

  def __init__(self, remote, local=None, common_suffix=None):
    suffix = self._FixPath(common_suffix or '')

    self.remote = os.path.join(remote, suffix)
    self.local = os.path.join(local or remote, suffix)

  @staticmethod
  def _FixPath(path_s):
    parts = [part for part in path_s.strip('/').split('/') if part]

    if not parts:
      return ''

    return os.path.join(*parts)

  def _GetRemote(self):
    return self._remote

  def _SetRemote(self, path_s):
    self._remote = self._FixPath(path_s)

  remote = property(_GetRemote, _SetRemote)

  def _GetLocal(self):
    return self._local

  def _SetLocal(self, path_s):
    self._local = self._FixPath(path_s)

  local = property(_GetLocal, _SetLocal)

  def GetAbsolute(self, depot, client):
    return (os.path.join('//', depot, self.remote),
            os.path.join('//', client, self.local))

  def __str__(self):
    return '%s(%s => %s)' % (self.__class__.__name__, self.remote, self.local)


class View(collections.MutableSet):
  """Keeps all information about local client required to work with perforce."""

  def __init__(self, depot, mappings=None, client=None):
    self.depot = depot

    if client:
      self.client = client

    self._mappings = set(mappings or [])

  @staticmethod
  def _FixRoot(root_s):
    parts = root_s.strip('/').split('/', 1)

    if len(parts) != 1:
      return None

    return parts[0]

  def _GetDepot(self):
    return self._depot

  def _SetDepot(self, depot_s):
    depot = self._FixRoot(depot_s)
    assert depot, 'Not a valid depot name: "%s".' % depot_s
    self._depot = depot

  depot = property(_GetDepot, _SetDepot)

  def _GetClient(self):
    return self._client

  def _SetClient(self, client_s):
    client = self._FixRoot(client_s)
    assert client, 'Not a valid client name: "%s".' % client_s
    self._client = client

  client = property(_GetClient, _SetClient)

  def add(self, mapping):
    assert type(mapping) is PathMapping
    self._mappings.add(mapping)

  def discard(self, mapping):
    assert type(mapping) is PathMapping
    self._mappings.discard(mapping)

  def __contains__(self, value):
    return value in self._mappings

  def __len__(self):
    return len(self._mappings)

  def __iter__(self):
    return iter(mapping for mapping in self._mappings)

  def AbsoluteMappings(self):
    return iter(mapping.GetAbsolute(self.depot, self.client)
                for mapping in self._mappings)


class CommandsFactory(object):
  """Creates shell commands used for interaction with Perforce."""

  def __init__(self, checkout_dir, p4view, name=None, port=None):
    self.port = port or 'perforce2:2666'
    self.view = p4view
    self.view.client = name or 'p4-automation-$HOSTNAME-$JOB_ID'
    self.checkout_dir = checkout_dir
    self.p4config_path = os.path.join(self.checkout_dir, '.p4config')

  def Initialize(self):
    return cmd.Chain('mkdir -p %s' % self.checkout_dir, 'cp ~/.p4config %s' %
                     self.checkout_dir, 'chmod u+w %s' % self.p4config_path,
                     'echo "P4PORT=%s" >> %s' % (self.port, self.p4config_path),
                     'echo "P4CLIENT=%s" >> %s' %
                     (self.view.client, self.p4config_path))

  def Create(self):
    # TODO(kbaclawski): Could we support value list for options consistently?
    mappings = ['-a \"%s %s\"' % mapping
                for mapping in self.view.AbsoluteMappings()]

    # First command will create client with default mappings.  Second one will
    # replace default mapping with desired.  Unfortunately, it seems that it
    # cannot be done in one step.  P4EDITOR is defined to /bin/true because we
    # don't want "g4 client" to enter real editor and wait for user actions.
    return cmd.Wrapper(
        cmd.Chain(
            cmd.Shell('g4', 'client'),
            cmd.Shell('g4', 'client', '--replace', *mappings)),
        env={'P4EDITOR': '/bin/true'})

  def SaveSpecification(self, filename=None):
    return cmd.Pipe(cmd.Shell('g4', 'client', '-o'), output=filename)

  def Sync(self, revision=None):
    sync_arg = '...'
    if revision:
      sync_arg = '%s@%s' % (sync_arg, revision)
    return cmd.Shell('g4', 'sync', sync_arg)

  def SaveCurrentCLNumber(self, filename=None):
    return cmd.Pipe(
        cmd.Shell('g4', 'changes', '-m1', '...#have'),
        cmd.Shell('sed', '-E', '"s,Change ([0-9]+) .*,\\1,"'),
        output=filename)

  def Remove(self):
    return cmd.Shell('g4', 'client', '-d', self.view.client)

  def SetupAndDo(self, *commands):
    return cmd.Chain(self.Initialize(),
                     self.InCheckoutDir(self.Create(), *commands))

  def InCheckoutDir(self, *commands):
    return cmd.Wrapper(cmd.Chain(*commands), cwd=self.checkout_dir)

  def CheckoutFromSnapshot(self, snapshot):
    cmds = cmd.Chain()

    for mapping in self.view:
      local_path, file_part = mapping.local.rsplit('/', 1)

      if file_part == '...':
        remote_dir = os.path.join(snapshot, local_path)
        local_dir = os.path.join(self.checkout_dir, os.path.dirname(local_path))

        cmds.extend([
            cmd.Shell('mkdir', '-p', local_dir), cmd.Shell(
                'rsync', '-lr', remote_dir, local_dir)
        ])

    return cmds