~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/arch/backends/tla.py

  • Committer: Robert Collins
  • Date: 2005-09-13 15:11:39 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913151139-9ac920fc9d7bda31
TODOification

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# arch-tag: david@allouche.net - 2003-11-17 15:29:01 469107000
 
2
# Copyright (C) 2003  John Goerzen, David Allouche
 
3
# <jgoerzen@complete.org> <david@allouche.net>
 
4
#
 
5
#    This program is free software; you can redistribute it and/or modify
 
6
#    it under the terms of the GNU General Public License as published by
 
7
#    the Free Software Foundation; either version 2 of the License, or
 
8
#    (at your option) any later version.
 
9
#
 
10
#    This program is distributed in the hope that it will be useful,
 
11
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
#    GNU General Public License for more details.
 
14
#
 
15
#    You should have received a copy of the GNU General Public License
 
16
#    along with this program; if not, write to the Free Software
 
17
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
 
19
"""Construction of tla commands
 
20
"""
 
21
 
 
22
import os.path
 
23
import re
 
24
from arch.pathname import DirName, FileName
 
25
from arch import errors
 
26
from arch._escaping import *
 
27
 
 
28
tlasyn = None
 
29
tlaobj = None
 
30
 
 
31
 
 
32
def gettlasyntax():
 
33
    global tlasyn, tlaobj
 
34
    if tlasyn != None:
 
35
        return tlasyn
 
36
    if text_cmd(['-V']).splitlines()[0].find('tla-1.0.') != -1:
 
37
        tlasyn = '1.0'
 
38
        tlaobj = Tla10()
 
39
    else:
 
40
        tlasyn = '1.1'
 
41
        tlaobj = Tla11()
 
42
    tlaobj.name_escaping = (0 == status_cmd(['escape'], (0,1)))
 
43
    return tlasyn
 
44
 
 
45
 
 
46
class Tla10:
 
47
    tagging_method = 'tagging-method'
 
48
    add = 'add-tag'
 
49
    move = 'move-tag'
 
50
    delete = 'delete-tag'
 
51
    update = 'update --in-place .'
 
52
    replay = 'replay --in-place .'
 
53
    ancestry_graph = None
 
54
    log_versions = 'logs'
 
55
    log_ls = 'log-ls'
 
56
    merges = None
 
57
    inventory_ids = ('inventory', '--tags')
 
58
    has_register_archive_wo_name = False
 
59
    get_changeset = 'get-patch'
 
60
 
 
61
 
 
62
class Tla11:
 
63
    tagging_method = 'id-tagging-method'
 
64
    add = 'add'
 
65
    move = 'move'
 
66
    delete = 'delete'
 
67
    update = 'update'
 
68
    replay = 'replay'
 
69
    ancestry_graph = 'ancestry-graph'
 
70
    log_versions = 'log-versions'
 
71
    log_ls = 'logs'
 
72
    merges = 'merges'
 
73
    inventory_ids = ('inventory', '--ids')
 
74
    has_register_archive_wo_name = True
 
75
    get_changeset = 'get-changeset'
 
76
 
 
77
 
 
78
def cmd():
 
79
    global tlaobj
 
80
    gettlasyntax()
 
81
    return tlaobj
 
82
 
 
83
 
 
84
### Utility functions ###
 
85
 
 
86
def _check_expected_param(expected):
 
87
    msg_format = ("'expected' param must be a sequence of positive"
 
88
                  " integers but was: %r")
 
89
    try:
 
90
        iterator = iter(expected)
 
91
    except TypeError:
 
92
        raise TypeError, msg_format % expected
 
93
    for status in iterator:
 
94
        if not isinstance(status, int):
 
95
            raise TypeError, msg_format % expected
 
96
        if status < 0:
 
97
            raise ValueError, msg_format % expected
 
98
 
 
99
 
 
100
### Subrocess spawning ###
 
101
 
 
102
def _spawner():
 
103
    import arch
 
104
    return arch.backend.spawner
 
105
 
 
106
def null_cmd(args, chdir=None):
 
107
    status = _spawner().status_cmd(args, chdir, (0,))
 
108
    assert status == 0
 
109
 
 
110
def one_cmd(args, chdir=None):
 
111
    status, line = status_one_cmd(args, chdir, (0,))
 
112
    assert status == 0
 
113
    return line
 
114
 
 
115
def sequence_cmd(args, chdir=None, expected=(0,)):
 
116
    _check_expected_param(expected)
 
117
    return _spawner().sequence_cmd(args, chdir, expected)
 
118
 
 
119
def text_cmd(args, chdir=None):
 
120
    status, text = _spawner().status_text_cmd(args, chdir, (0,))
 
121
    assert status == 0
 
122
    return text
 
123
 
 
124
def status_cmd(args, expected):
 
125
    _check_expected_param(expected)
 
126
    return _spawner().status_cmd(args, None, expected)
 
127
 
 
128
def status_one_cmd(args, chdir, expected):
 
129
    _check_expected_param(expected)
 
130
    seq = sequence_cmd(args, chdir, expected)
 
131
    try:
 
132
        out = seq.next()
 
133
    except StopIteration:
 
134
        return seq.status, None
 
135
    tail = list(seq)
 
136
    if not tail:
 
137
        return seq.status, out
 
138
    raise AssertionError, (
 
139
        'one liner actually produced multiline output:\n%s'
 
140
        % '\n'.join([out] + tail))
 
141
 
 
142
def status_text_cmd(args, expected):
 
143
    _check_expected_param(expected)
 
144
    return _spawner().status_text_cmd(args, None, expected)
 
145
 
 
146
 
 
147
### tla commands ###
 
148
 
 
149
# User Commands
 
150
 
 
151
def my_id():
 
152
    status, output = status_one_cmd(('my-id',), None, (0,2))
 
153
    # status is 2 if the user id is not set.
 
154
    # maybe an exception should be thrown instead of returning None.
 
155
    if status != 0: return None
 
156
    return output
 
157
 
 
158
 
 
159
def set_my_id(newid):
 
160
    null_cmd(('my-id', newid))
 
161
 
 
162
 
 
163
def default_archive():
 
164
    status, output = status_one_cmd(('my-default-archive',), None, (0,1))
 
165
    if status != 0: return None
 
166
    return output
 
167
 
 
168
 
 
169
def set_default_archive(archive):
 
170
    null_cmd(('my-default-archive', archive))
 
171
 
 
172
def make_archive(archive, location, signed=False, listing=False):
 
173
    args = ['make-archive']
 
174
    if signed: args.append('--signed')
 
175
    if listing: args.append('--listing')
 
176
    args.extend((archive, location))
 
177
    null_cmd(args)
 
178
 
 
179
def make_archive_mirror(master, name, location, signed=False, listing=False):
 
180
    args = ['make-archive', '--mirror', master]
 
181
    if signed: args.append('--signed')
 
182
    if listing: args.append('--listing')
 
183
    args.extend((name, location))
 
184
    return null_cmd(args)
 
185
 
 
186
def register_archive(archive, location):
 
187
    if archive is None:
 
188
        if not cmd().has_register_archive_wo_name:
 
189
            raise NotImplementedError, \
 
190
                  "register-archive without an archive name"\
 
191
                  " is not implemented in tla 1.0"
 
192
        null_cmd(('register-archive', location))
 
193
    else:
 
194
        null_cmd(('register-archive', archive, location))
 
195
 
 
196
 
 
197
def unregister_archive(archive):
 
198
    null_cmd(('register-archive', '--delete', archive))
 
199
 
 
200
def whereis_archive(archive):
 
201
    return one_cmd(('whereis-archive', archive))
 
202
 
 
203
def archive_meta_info(archive, key):
 
204
    status, output = status_one_cmd(
 
205
        ('archive-meta-info', '--archive', archive, key), None, (0, 1))
 
206
    if status == 1:
 
207
        raise KeyError, "No such meta-info in archive %s: %s" % (archive, key)
 
208
    return output
 
209
 
 
210
def archives():
 
211
    return sequence_cmd(('archives', '--names'))
 
212
 
 
213
def archive_registered(archive):
 
214
    return archive in archives()
 
215
 
 
216
 
 
217
# Project Tree commands
 
218
 
 
219
def init_tree(dir, version=None, nested=False):
 
220
    args = ['init-tree', '--dir', dir]
 
221
    if nested: args.append('--nested')
 
222
    if version is not None: args.append(version)
 
223
    null_cmd(args)
 
224
 
 
225
def tree_root(dir):
 
226
    status, output = status_text_cmd(('tree-root', dir), (0,1))
 
227
    if status == 1: raise errors.TreeRootError(dir)
 
228
    return output[:-1]
 
229
 
 
230
def tree_version(dir):
 
231
    vsn_name = dir/'{arch}'/'++default-version'
 
232
    try:
 
233
        return open(vsn_name, 'r').read().rstrip('\n')
 
234
    except IOError:
 
235
        if not os.path.exists(vsn_name): return None
 
236
        raise
 
237
 
 
238
def set_tree_version(dir, version):
 
239
    null_cmd(('set-tree-version', '--dir', dir, version))
 
240
 
 
241
def has_changes(dir):
 
242
    args = ('changes', '--dir', dir, '--quiet')
 
243
    return 1 == status_cmd(args, expected=(0,1))
 
244
 
 
245
def star_merge(dir, from_=None, reference=None, forward=False, diff3=False):
 
246
    args = ['star-merge', '--dir', dir]
 
247
    if reference is not None: args.extend(('--reference', reference))
 
248
    if forward: args.append('--forward')
 
249
    if diff3: args.append('--three-way')
 
250
    if from_ is not None: args.append(from_)
 
251
    # yes, we want to raise if there was a conflict, see the docstring
 
252
    # of arch.WorkingTree.star_merge for details.
 
253
    null_cmd(args)
 
254
 
 
255
def iter_star_merge(dir, from_=None, reference=None, forward=False,
 
256
                    diff3=False):
 
257
    args = ['star-merge', '--dir', dir]
 
258
    if reference is not None: args.extend(('--reference', reference))
 
259
    if forward: args.append('--forward')
 
260
    if diff3: args.append('--three-way')
 
261
    if from_ is not None: args.append(from_)
 
262
    return sequence_cmd(args, expected=(0,1))
 
263
 
 
264
def sync_tree(dir, revision):
 
265
    null_cmd(('sync-tree', '--dir', dir, revision))
 
266
 
 
267
def undo(dir, revision=None, output=None, quiet=False, throw_away=False):
 
268
    args = ['undo', '--dir', dir]
 
269
    if quiet: args.append('--quiet')
 
270
    if throw_away:
 
271
        args.append('--no-output')
 
272
    elif output:
 
273
        args.extend(('--output', output))
 
274
    if revision: args.append(revision)
 
275
    null_cmd(args)
 
276
 
 
277
def log_for_merge(dir):
 
278
    args = ['log-for-merge', '--dir', dir]
 
279
    return text_cmd(args)
 
280
 
 
281
 
 
282
def redo(dir, patch=None, keep=False, quiet=False):
 
283
    args = ['redo', '--dir', dir]
 
284
    if quiet: args.append('--quiet')
 
285
    if keep: args.append('--keep')
 
286
    if patch: args.extend(('--patch', patch))
 
287
    null_cmd(args)
 
288
 
 
289
def update(dir):
 
290
    """FIXME: add the other parameters."""
 
291
    ### Correct implementation
 
292
    # args = ['update', '--dir', dir]
 
293
    # return null_cmd(args)
 
294
    ### Work around bug in tla 1.2.1
 
295
    null_cmd(['update'], chdir=dir)
 
296
 
 
297
def replay(dir):
 
298
    """FIXME: add the other parameters."""
 
299
    args = ['replay', '--dir', dir]
 
300
    null_cmd(args)
 
301
 
 
302
 
 
303
# Project Tree Inventory commands
 
304
 
 
305
def __inventory_helper(dir, opts,
 
306
                       source, precious, backups, junk, unrecognized, trees,
 
307
                       directories, files, both, names, limit):
 
308
    __pychecker__ = 'maxargs=13'
 
309
    # Asserts there is only one kind and one type
 
310
    (source, precious, backups,
 
311
     junk, unrecognized, trees) = map(bool, (source, precious, backups,
 
312
                                             junk, unrecognized, trees))
 
313
    assert 1 == sum(map(int, (source, precious, backups,
 
314
                              junk, unrecognized, trees)))
 
315
    (directories, files, both) = map(bool, (directories, files, both))
 
316
    assert 1 == sum(map(int, (directories, files, both, trees)))
 
317
 
 
318
    if source: opts.append('--source')
 
319
    if precious: opts.append('--precious')
 
320
    if backups: opts.append('--backups')
 
321
    if junk: opts.append('--junk')
 
322
    if unrecognized: opts.append('--unrecognized')
 
323
    if trees: opts.append('--trees')
 
324
    if directories: opts.append('--directories')
 
325
    if files: opts.append('--files')
 
326
    if both: opts.append('--both')
 
327
    if names: opts.append('--names')
 
328
    if limit is not None: opts.append(limit)
 
329
    return sequence_cmd(opts, chdir=dir)
 
330
 
 
331
 
 
332
def iter_inventory(dir, source=False, precious=False, backups=False,
 
333
                   junk=False, unrecognized=False, trees=False,
 
334
                   directories=False, files=False, both=False, names=False,
 
335
                   limit=None):
 
336
    __pychecker__ = 'maxargs=12'
 
337
    return __inventory_helper(dir, ['inventory'], source, precious, backups,
 
338
                              junk, unrecognized, trees,
 
339
                              directories, files, both, names, limit)
 
340
 
 
341
 
 
342
def iter_inventory_ids(dir, source=False, precious=False, backups=False,
 
343
                       junk=False, unrecognized=False, trees=False,
 
344
                       directories=False, files=False, both=False,
 
345
                       limit=None):
 
346
    __pychecker__ = 'maxargs=11'
 
347
    for line in __inventory_helper(dir, list(cmd().inventory_ids),
 
348
                                   source, precious, backups,
 
349
                                   junk, unrecognized, trees,
 
350
                                   directories, files, both,
 
351
                                   names=False, limit=limit):
 
352
        yield line.split('\t')
 
353
 
 
354
 
 
355
def tagging_method(dir):
 
356
    return one_cmd((cmd().tagging_method, '--dir', dir))
 
357
 
 
358
def set_tagging_method(dir, method):
 
359
    null_cmd((cmd().tagging_method, '--dir', dir, method))
 
360
 
 
361
def add(file):
 
362
    null_cmd((cmd().add, file))
 
363
 
 
364
def delete(file):
 
365
    null_cmd((cmd().delete, file))
 
366
 
 
367
def move(src, dest):
 
368
    return null_cmd((cmd().move, src, dest))
 
369
 
 
370
def get_tag(file):
 
371
    status, output = status_one_cmd(('id', file), None, (0,1))
 
372
    if status == 1: return None
 
373
    return output.split('\t')[1]
 
374
 
 
375
 
 
376
# Patch Set Commands
 
377
 
 
378
def changeset(orig, mod, dest, files=()):
 
379
    return null_cmd(('changeset', orig, mod, dest) + files)
 
380
 
 
381
def iter_apply_changeset(changeset, tree, reverse=False):
 
382
    args = ['apply-changeset']
 
383
    if reverse: args.append('--reverse')
 
384
    args.extend((changeset, tree))
 
385
    return sequence_cmd(args, expected=(0,1))
 
386
 
 
387
 
 
388
# Archive Transaction Commands
 
389
 
 
390
def archive_setup(name):
 
391
    null_cmd(('archive-setup', name))
 
392
 
 
393
def import_(dir, log=None):
 
394
    args = ['import', '--dir', dir]
 
395
    if log is not None: args.extend(('--log', log))
 
396
    null_cmd(args)
 
397
 
 
398
def iter_commit(dir, version, log=None, strict=False, seal=False, fix=False,
 
399
           out_of_date_ok=False, file_list=None, base=None):
 
400
    args = ['commit']
 
401
    if log is not None: args.extend(('--log', log))
 
402
    if strict: args.append('--strict')
 
403
    assert not (seal and fix)
 
404
    if seal: args.append('--seal')
 
405
    if fix: args.append('--fix')
 
406
    if out_of_date_ok: args.append('--out-of-date-ok')
 
407
    if base is not None:
 
408
        args.extend(('--base', base))
 
409
    args.append(version)
 
410
    if file_list is not None:
 
411
        file_list = tuple(file_list)
 
412
        assert 0 < len(file_list)
 
413
        args.append('--')
 
414
        args.extend(map(str, file_list))
 
415
    return sequence_cmd(args, chdir=str(dir))
 
416
 
 
417
def get(revision, dir, link=False):
 
418
    command = ['get']
 
419
    if link: command.append('--link')
 
420
    command.extend((revision, dir))
 
421
    null_cmd(command)
 
422
 
 
423
def get_patch(revision, dir):
 
424
    P = NameParser(revision)
 
425
    # XXX Work around bug in 1.2.2rc2 and corresponding integration revisions
 
426
    # XXX where get-changeset would fail with "no default archive set".
 
427
    args = [cmd().get_changeset, '-A', P.get_archive(), P.get_nonarch(), dir]
 
428
    null_cmd(args)
 
429
 
 
430
def archive_mirror(from_, to, limit=None, no_cached=False, cached_tags=False):
 
431
    # --summary would be useful only in iter_archive_mirror
 
432
    args = ['archive-mirror']
 
433
    assert no_cached + cached_tags < 2
 
434
    if no_cached: args.append('--no-cached')
 
435
    if cached_tags: args.append('--cached-tags')
 
436
    args.append(from_)
 
437
    if to is not None: args.append(to)
 
438
    if limit is not None:
 
439
        assert 0 < len(limit)
 
440
        args.extend(limit)
 
441
    null_cmd(args)
 
442
 
 
443
# Archive Commands
 
444
 
 
445
def categories(archive):
 
446
    return sequence_cmd(('categories', '--archive', archive))
 
447
 
 
448
def branches(category):
 
449
    return sequence_cmd(('branches', category))
 
450
 
 
451
def versions(branch, reverse=False):
 
452
    if not reverse:
 
453
        return sequence_cmd(('versions', branch))
 
454
    else:
 
455
        return sequence_cmd(('versions', '--reverse', branch))
 
456
 
 
457
def revisions(version, reverse=False):
 
458
    if not reverse:
 
459
        return sequence_cmd(('revisions', version))
 
460
    else:
 
461
        return sequence_cmd(('revisions', '--reverse', version))
 
462
 
 
463
 
 
464
def category_exists(archive, category):
 
465
    if not archive_registered(archive):
 
466
        raise errors.ArchiveNotRegistered(archive)
 
467
    else:
 
468
        return category in categories(archive)
 
469
 
 
470
 
 
471
def branch_exists(archive, branch):
 
472
    category = NameParser(branch).get_category()
 
473
    if not category_exists(archive, category):
 
474
        return False
 
475
    else:
 
476
        return branch in branches(archive+"/"+category)
 
477
 
 
478
 
 
479
def version_exists(archive, version):
 
480
    package = NameParser(version).get_package()
 
481
    if not branch_exists(archive, package):
 
482
        return False
 
483
    else:
 
484
        return version in versions(archive+"/"+package)
 
485
 
 
486
 
 
487
def revision_exists(archive, version, patchlevel):
 
488
    try:
 
489
        return patchlevel in revisions(archive+"/"+version)
 
490
    except Exception:
 
491
        if not version_exists(archive, version):
 
492
            return False
 
493
        else:
 
494
            raise
 
495
 
 
496
def cat_archive_log(revision):
 
497
    return text_cmd(('cat-archive-log', revision))
 
498
 
 
499
 
 
500
def cacherev(revision, cache=None):
 
501
    if cache is None:
 
502
        return null_cmd(('cacherev', revision))
 
503
    else:
 
504
        return null_cmd(('cacherev', '--cache', cache, revision))
 
505
 
 
506
 
 
507
def cachedrevs(version):
 
508
    return sequence_cmd(('cachedrevs', version))
 
509
 
 
510
 
 
511
def uncacherev(revision):
 
512
    null_cmd(('uncacherev', revision))
 
513
 
 
514
 
 
515
def iter_merges(version1, version2=None, reverse=False, metoo=True):
 
516
    subcmd = cmd().merges
 
517
    if subcmd is None:
 
518
        raise NotImplementedError, \
 
519
              "merges is not implemented in tla 1.0"""
 
520
    args = [ subcmd ]
 
521
    if reverse: args.append('--reverse')
 
522
    args.append('--full')
 
523
    args.append(version1)
 
524
    if version2 is not None: args.append(version2)
 
525
    for line in sequence_cmd(args):
 
526
        target, source = line.split('\t')
 
527
        if metoo or '%s--%s' % (version1, target) != source:
 
528
            yield target, source
 
529
 
 
530
 
 
531
def iter_ancestry_graph(revision, merges=False, reverse=False):
 
532
    subcmd = cmd().ancestry_graph
 
533
    if subcmd is None:
 
534
        raise NotImplementedError, \
 
535
              "ancestry-graph is not implemented in tla 1.0"
 
536
    args = [ subcmd ]
 
537
    if merges: args.append('--merges')
 
538
    if reverse: args.append('--reverse')
 
539
    args.append(revision)
 
540
    for line in sequence_cmd(args):
 
541
        yield line.split('\t')
 
542
 
 
543
 
 
544
def ancestor(revision):
 
545
    subcmd = cmd().ancestry_graph
 
546
    if subcmd is None:
 
547
        raise NotImplementedError, \
 
548
              "ancestry-graph is not implemented in tla 1.0"
 
549
    rvsn = one_cmd((subcmd, '--immediate', revision))
 
550
    if rvsn == '(null)': return None
 
551
    # --immediate always return a fully qualified revision
 
552
    return rvsn
 
553
 
 
554
def previous(revision):
 
555
    subcmd = cmd().ancestry_graph
 
556
    if subcmd is None:
 
557
        raise NotImplementedError, \
 
558
              "ancestry-graph is not implemented in tla 1.0"
 
559
    rvsn = one_cmd((subcmd, '--previous', revision))
 
560
    if rvsn == '(null)': return None
 
561
    # --previous always returns a nonarch revision name part
 
562
    return NameParser(revision).get_archive()+'/'+rvsn
 
563
 
 
564
 
 
565
# Patch Log Commands
 
566
 
 
567
def make_log(dir, version=None):
 
568
    args = ['make-log', '--dir', dir]
 
569
    if version is not None:
 
570
        args.append(version)
 
571
    return text_cmd(args)[:-1]
 
572
 
 
573
def log_name(dir, version=None):
 
574
    if version is None:
 
575
        version = tree_version(dir)
 
576
    parse = NameParser(version)
 
577
    return dir/FileName('++log.%s--%s'
 
578
                        % (parse.get_nonarch(), parse.get_archive()))
 
579
 
 
580
def add_log_version(dir, version):
 
581
    null_cmd(('add-log-version', '--dir', dir, version))
 
582
 
 
583
def remove_log_version(dir, version):
 
584
    null_cmd(('remove-log-version', '--dir', dir, version))
 
585
 
 
586
 
 
587
def iter_log_versions(dir, reverse=False,
 
588
                      archive=None, category=None, branch=None, version=None):
 
589
    opts = (cmd().log_versions, '--dir', dir)
 
590
    if reverse: opts += ('--reverse',)
 
591
    if archive: opts += ('--archive', archive)
 
592
    if category: opts += ('--category', category)
 
593
    if branch: opts += ('--branch', branch)
 
594
    if version: opts += ('--vsn', version)
 
595
    return sequence_cmd(opts)
 
596
 
 
597
 
 
598
def iter_log_ls(dir, version, reverse=False):
 
599
    opts = [ cmd().log_ls, '--full', '--dir', dir ]
 
600
    if reverse: opts.append('--reverse')
 
601
    opts.append(version)
 
602
    return sequence_cmd(opts)
 
603
 
 
604
 
 
605
def cat_log(dir, revision):
 
606
    return text_cmd(('cat-log', '--dir', dir, revision))
 
607
 
 
608
 
 
609
# Multi-project Configuration Commands
 
610
 
 
611
# Commands for Branching and Merging
 
612
 
 
613
def tag(source_revision, tag_version):
 
614
    null_cmd(('tag', source_revision, tag_version))
 
615
 
 
616
def iter_delta (orig, mod, dest):
 
617
    args = ['delta', orig, mod, dest]
 
618
    return sequence_cmd(args)
 
619
 
 
620
 
 
621
# Local Cache Commands
 
622
 
 
623
def file_find(tree, file_name, revision):
 
624
    args = ('file-find', '--new-file', file_name, revision)
 
625
    # XXX Work around missing --silent option, ignore chatter
 
626
    output = text_cmd(args, chdir=tree)
 
627
    path = output.splitlines()[-1]
 
628
    if path == '/dev/null': return None
 
629
    if cmd().name_escaping:
 
630
        return name_unescape(path)
 
631
    else:
 
632
        return path
 
633
 
 
634
def iter_pristines(dir_name):
 
635
    args = ('pristines', '--dir', dir_name)
 
636
    return sequence_cmd(args)
 
637
 
 
638
def add_pristine(dir_name, revision):
 
639
    args = ('add-pristine', '--dir', dir_name, revision)
 
640
    null_cmd(args)
 
641
 
 
642
def find_pristine(dir_name, revision):
 
643
    args = ('find-pristine', '--dir', dir_name, revision)
 
644
    return one_cmd(args)
 
645
 
 
646
 
 
647
# Revision Library Commands
 
648
 
 
649
def iter_revision_libraries():
 
650
    args = ('my-revision-library', '--silent')
 
651
    try:
 
652
        for val in sequence_cmd(args):
 
653
            yield val
 
654
    except errors.ExecProblem, e:
 
655
        if e.proc.error != \
 
656
            "my-revision-library: no revision library path set\n":
 
657
            raise
 
658
 
 
659
def register_revision_library(dirname):
 
660
    null_cmd(('my-revision-library', '--silent', dirname))
 
661
 
 
662
def unregister_revision_library(dirname):
 
663
    null_cmd(('my-revision-library', '--silent', '--delete', dirname))
 
664
 
 
665
def library_archives():
 
666
    return sequence_cmd(('library-archives',))
 
667
 
 
668
def library_categories(archive):
 
669
    return sequence_cmd(('library-categories', '--archive', archive))
 
670
 
 
671
def library_branches(category):
 
672
    return  sequence_cmd(('library-branches', category))
 
673
 
 
674
def library_versions(branch, reverse=False):
 
675
    if not reverse:
 
676
        return sequence_cmd(('library-versions', branch))
 
677
    else:
 
678
        return sequence_cmd(('library-versions', '--reverse', branch))
 
679
 
 
680
def library_revisions(version, reverse=False):
 
681
    if not reverse:
 
682
        return sequence_cmd(('library-revisions', version))
 
683
    else:
 
684
        return sequence_cmd(('library-revisions', '--reverse', version))
 
685
 
 
686
 
 
687
def library_add(revision, library):
 
688
    args = ['library-add']
 
689
    if library is not None:
 
690
        args.extend(['-L', library])
 
691
    args.append(revision)
 
692
    return null_cmd(('library-add', revision))
 
693
 
 
694
def library_remove(revision):
 
695
    null_cmd(('library-remove', revision))
 
696
 
 
697
def library_find(revision):
 
698
    return text_cmd(('library-find', revision))[:-1]
 
699
 
 
700
def library_log(revision):
 
701
    return text_cmd(('library-log', revision))
 
702
 
 
703
 
 
704
### Name parsing ###
 
705
 
 
706
class ForkNameParser(str):
 
707
 
 
708
    """Parse Arch names with with tla.
 
709
 
 
710
    All operations run tla, this is generally too expensive for
 
711
    practical use. Use NameParser instead, which is implemented
 
712
    natively.
 
713
 
 
714
    This class is retained for testing purposes.
 
715
    """
 
716
 
 
717
    def __valid_package_name(self, opt, tolerant=True):
 
718
        opts = [ 'valid-package-name' ]
 
719
        if tolerant: opts.append('--tolerant')
 
720
        if opt: opts.append(opt)
 
721
        opts.append(self)
 
722
        return 0 == status_cmd(opts, expected=(0,1))
 
723
 
 
724
    def __parse_package_name(self, opt):
 
725
        opts = [ 'parse-package-name' ]
 
726
        if opt: opts.append(opt)
 
727
        opts.append(self)
 
728
        status, retval = status_one_cmd(opts, None, (0,1))
 
729
        return retval
 
730
 
 
731
    def is_category(self):
 
732
        return self.__valid_package_name('--category', tolerant=False)
 
733
    def is_package(self):
 
734
        return self.__valid_package_name('--package', tolerant=False)
 
735
    def is_version(self):
 
736
        return self.__valid_package_name('--vsn', tolerant=False)
 
737
    def is_patchlevel(self):
 
738
        return self.__valid_package_name('--patch-level', tolerant=False)
 
739
 
 
740
    def has_archive(self):
 
741
        return self.__valid_package_name('--archive')
 
742
    def has_category(self):
 
743
        return self.__valid_package_name('--category')
 
744
    def has_package(self):
 
745
        return self.__valid_package_name('--package')
 
746
    def has_version(self):
 
747
        return self.__valid_package_name('--vsn')
 
748
    def has_patchlevel(self):
 
749
        return self.__valid_package_name('--patch-level')
 
750
 
 
751
    def get_archive(self):
 
752
        return self.__parse_package_name('--arch')
 
753
    def get_nonarch(self):
 
754
        return self.__parse_package_name('--non-arch')
 
755
    def get_category(self):
 
756
        return self.__parse_package_name('--category')
 
757
    def get_branch(self):
 
758
        return self.__parse_package_name('--branch')
 
759
    def get_package(self):
 
760
        return self.__parse_package_name('--package')
 
761
    def get_version(self):
 
762
        if not self.has_version(): return None # work around a tla bug
 
763
        return self.__parse_package_name('--vsn')
 
764
    def get_package_version(self):
 
765
        return self.__parse_package_name('--package-version')
 
766
    def get_patchlevel(self):
 
767
        if not self.has_patchlevel(): return None # work around a tla bug
 
768
        return self.__parse_package_name('--patch-level')
 
769
 
 
770
 
 
771
class NameParser(str):
 
772
 
 
773
    """Parser for names in Arch archive namespace.
 
774
 
 
775
    Implements name parsing natively for performance reasons. It
 
776
    should behave exactly as tla, any discrepancy is to be considered
 
777
    a bug, unless tla is obviously buggy.
 
778
 
 
779
    Bare names of archives, category, branch, versions ids, and
 
780
    unqualified patchlevel names are not part of the archive
 
781
    namespace. They can be validated using static methods.
 
782
 
 
783
    :group Specificity level: is_category, is_package, is_version
 
784
 
 
785
    :group Presence name components: has_archive, has_category, has_branch,
 
786
        has_package, has_version, has_revision, has_patchlevel
 
787
 
 
788
    :group Getting name components: get_archive, get_nonarch, get_category,
 
789
        get_branch, get_package, get_version, get_package_version,
 
790
        get_patchlevel
 
791
 
 
792
    :group Validating name components: is_archive_name, is_category_name,
 
793
        is_branch_name, is_version_id, is_patchlevel
 
794
    """
 
795
 
 
796
    __archive_regex = re.compile('^[-a-zA-Z0-9]+(\.[-a-zA-Z0-9]+)*@'
 
797
                                 '[-a-zA-Z0-9.]*$')
 
798
    __name_regex = re.compile('^[a-zA-Z]([a-zA-Z0-9]|-[a-zA-Z0-9])*$')
 
799
    __version_regex = re.compile('^[0-9]+(\\.[0-9]+)*$')
 
800
    #__level_regex = re.compile('^base-0|version-0|(patch|versionfix)-[0-9]+$')
 
801
    # tla accepts --version-12, so mimick that:
 
802
    __level_regex = re.compile('^base-0|(version|patch|versionfix)-[0-9]+$')
 
803
 
 
804
    def __init__(self, s):
 
805
        """Create a parser object for the given string.
 
806
 
 
807
        :param s: string to parse.
 
808
        :type s: str
 
809
        """
 
810
        str.__init__(s)
 
811
        parts = self.__parse()
 
812
        if parts:
 
813
            self.__valid = True
 
814
        else:
 
815
            parts = None, None, None, None, None, None
 
816
            self.__valid = False
 
817
        (self.__archive, self.__nonarch, self.__category,
 
818
         self.__branch, self.__version, self.__level) = parts
 
819
 
 
820
    def __parse(self):
 
821
        parts = self.split('/')
 
822
        if len(parts) == 1:
 
823
            archive, nonarch = None, parts[0]
 
824
        elif len(parts) == 2:
 
825
            archive, nonarch = parts
 
826
        else:
 
827
            return False
 
828
 
 
829
        parts = nonarch.split('--')
 
830
        category, branch, version, level = None, None, None, None
 
831
        if len(parts) == 1:
 
832
            category = parts[0]
 
833
        elif len(parts) == 2:
 
834
            if self.__name_regex.match(parts[1]):
 
835
                category, branch = parts
 
836
            else:
 
837
                category, version = parts
 
838
        elif len(parts) == 3:
 
839
            if self.__name_regex.match(parts[1]):
 
840
                category, branch, version = parts
 
841
            else:
 
842
                category, version, level = parts
 
843
        elif len(parts) == 4:
 
844
            category, branch, version, level = parts
 
845
        else:
 
846
            return False
 
847
 
 
848
        if archive and not self.__archive_regex.match(archive):
 
849
            return False
 
850
        elif not self.__name_regex.match(category):
 
851
            return False
 
852
        elif branch and not self.__name_regex.match(branch):
 
853
            return False
 
854
        elif not version and not level: pass
 
855
        elif not self.__version_regex.match(version):
 
856
            return False
 
857
        elif not level: pass
 
858
        elif not self.__level_regex.match(level):
 
859
            return False
 
860
 
 
861
        return archive, nonarch, category, branch, version, level
 
862
 
 
863
    def is_category(self):
 
864
        """Is this a category name?
 
865
 
 
866
        :rtype: bool
 
867
        """
 
868
        return bool(self.__category) and not (self.__branch or self.__version)
 
869
 
 
870
    def is_package(self):
 
871
        """Is this a package name (category or branch name)?
 
872
 
 
873
        :rtype: bool
 
874
        """
 
875
        return bool(self.__category) and not self.__version
 
876
    def is_version(self):
 
877
        """Is this a version name?
 
878
 
 
879
        :rtype: bool
 
880
        """
 
881
        return bool(self.__version) and not self.__level
 
882
 
 
883
    def has_archive(self):
 
884
        """Does this include an archive name?
 
885
 
 
886
        :rtype: bool
 
887
        """
 
888
        return bool(self.__archive)
 
889
 
 
890
    def has_category(self):
 
891
        """Does this include an category name?
 
892
 
 
893
        :rtype: bool
 
894
        """
 
895
        return bool(self.__category)
 
896
 
 
897
    def has_package(self):
 
898
        """Does this include an package name?
 
899
 
 
900
        :rtype: bool
 
901
        """
 
902
        return bool(self.__category)
 
903
 
 
904
    def has_version(self):
 
905
        """Does this include a version name?
 
906
 
 
907
        :rtype: bool
 
908
        """
 
909
        return bool(self.__version)
 
910
 
 
911
    def has_patchlevel(self):
 
912
        """Does this include a revision name?
 
913
 
 
914
        :rtype: bool
 
915
        """
 
916
        return bool(self.__level)
 
917
 
 
918
    def get_archive(self):
 
919
        """Get the archive part of the name
 
920
 
 
921
        :return: archive part of the name, or the default archive name, or None
 
922
            if the name is invalid.
 
923
        :rtype: str, None
 
924
        """
 
925
        if not self.__valid: return None
 
926
        if not self.__archive: return default_archive()
 
927
        return self.__archive
 
928
 
 
929
    def get_nonarch(self):
 
930
        """Get Non-archive part of the name
 
931
 
 
932
        :return: the name without the archive component, or None if the name is
 
933
            invalid or has no archive component.
 
934
        :rtype: str, None
 
935
        """
 
936
        return self.__nonarch
 
937
 
 
938
    def get_category(self):
 
939
        """Get the Category name
 
940
 
 
941
        :return: part of the name which identifies the category within
 
942
            the archive, or None if the name is invalid or has no category
 
943
            component.
 
944
        :rtype: str, None
 
945
        """
 
946
        return self.__category
 
947
 
 
948
    def get_branch(self):
 
949
        """Get the branch part of name
 
950
 
 
951
        :return: part of the name which identifies the branch within the
 
952
            category, or None if the name is invalid or the empty string if the
 
953
            name has no branch component.
 
954
        :rtype: str, None
 
955
        """
 
956
        if not self.__valid: return None
 
957
        if not self.__branch: return str()
 
958
        return self.__branch
 
959
 
 
960
    def get_package(self):
 
961
        """Get the package name
 
962
 
 
963
        :return: part of the name including the category part and branch part
 
964
            (if present) of the name, or None if the name is not valid.
 
965
        :rtype: str, None
 
966
        """
 
967
        if not self.__valid: return None
 
968
        if self.__branch is None: return self.__category
 
969
        return self.__category + '--' + self.__branch
 
970
 
 
971
    def get_version(self):
 
972
        """Get the version id part of the name
 
973
 
 
974
        :return: part of the name identifying a version in a branch, or None if
 
975
            the name is invalid or does not contain a version id.
 
976
        :rtype: str, None
 
977
        """
 
978
        return self.__version
 
979
 
 
980
    def get_package_version(self):
 
981
        """Get the unqualified version name
 
982
 
 
983
        :return: part of the name identifying a version in an archive, or None
 
984
            if the name does not contain a version id or is invalid.
 
985
        :rtype: str, None
 
986
        """
 
987
        if not self.__version: return None
 
988
        return self.get_package() + '--' + self.__version
 
989
 
 
990
    def get_patchlevel(self):
 
991
        """Get the patch-level part of the name
 
992
 
 
993
        :return: part of the name identifying a patch in a version, or None if
 
994
            the name is not a revision or is invalid.
 
995
        :rtype: str, None
 
996
        """
 
997
        return self.__level
 
998
 
 
999
    def is_archive_name(klass, s):
 
1000
        """Is this string a valid archive name?
 
1001
 
 
1002
        :param s: string to validate.
 
1003
        :type s: str
 
1004
        :rtype: bool
 
1005
        """
 
1006
        return bool(klass.__archive_regex.match(s))
 
1007
    is_archive_name = classmethod(is_archive_name)
 
1008
 
 
1009
    def is_category_name(klass, s):
 
1010
        """Is this string a valid category name?
 
1011
 
 
1012
        Currently does the same thing as is_branch_name, but that might
 
1013
        change in the future when the namespace evolves and it is more
 
1014
        expressive to have different functions.
 
1015
 
 
1016
        :param s: string to validate.
 
1017
        :type s: str
 
1018
        :rtype: bool
 
1019
        """
 
1020
        return bool(klass.__name_regex.match(s))
 
1021
    is_category_name = classmethod(is_category_name)
 
1022
 
 
1023
    def is_branch_name(klass, s):
 
1024
        """Is this string a valid category name?
 
1025
 
 
1026
        Currently does the same thing as is_category_name, but that might
 
1027
        change in the future when the namespace evolves and it is more
 
1028
        expressive to have different functions.
 
1029
 
 
1030
        :param s: string to validate.
 
1031
        :type s: str
 
1032
        :rtype: bool
 
1033
        """
 
1034
        return bool(klass.__name_regex.match(s))
 
1035
    is_branch_name = classmethod(is_branch_name)
 
1036
 
 
1037
    def is_version_id(klass, s):
 
1038
        """Is this string a valid version id?
 
1039
 
 
1040
        :param s: string to validate.
 
1041
        :type s: str
 
1042
        :rtype: bool
 
1043
        """
 
1044
        return bool(klass.__version_regex.match(s))
 
1045
    is_version_id = classmethod(is_version_id)
 
1046
 
 
1047
    def is_patchlevel(klass, s):
 
1048
        """Is this string a valid unqualified patch-level name?
 
1049
 
 
1050
        :param s: string to validate.
 
1051
        :type s: str
 
1052
        :rtype: bool
 
1053
        """
 
1054
        return bool(klass.__level_regex.match(s))
 
1055
    is_patchlevel = classmethod(is_patchlevel)
 
1056
 
 
1057
 
 
1058
### Pika escaping ###
 
1059
 
 
1060
def tla_name_escape(text):
 
1061
   return text_cmd(('escape', text))
 
1062
 
 
1063
def tla_name_unescape(text):
 
1064
    return text_cmd(('escape', '--unescaped', text))
 
1065
 
 
1066
 
 
1067
### Misc features ###
 
1068
 
 
1069
def in_tree(dir):
 
1070
    """Return True if dir is, or is inside, a Arch source tree."""
 
1071
    # Must use tree-root and not tree-version because the tree-version
 
1072
    # may be unset (after init-tree)
 
1073
    status = status_cmd(('tree-root', dir), expected=(0,1))
 
1074
    return not status
 
1075
 
 
1076
 
 
1077
def is_tree_root(dir):
 
1078
    """Return True if dir is a Arch source tree."""
 
1079
    opts = [ 'tree-root', dir ]
 
1080
    status, out = status_text_cmd(opts, expected=(0,1))
 
1081
    if status != 0: return False
 
1082
    return os.path.realpath(dir) == out[:-1]
 
1083
 
 
1084
 
 
1085
def has_explicit_id(path):
 
1086
    assert os.path.exists(path)
 
1087
    arch_ids = DirName('.arch-ids')
 
1088
    if os.path.isfile(path):
 
1089
        dir_, base = FileName(path).splitname()
 
1090
        return os.path.exists(dir_/arch_ids/(base+'.id'))
 
1091
    if os.path.isdir(path):
 
1092
        return os.path.exists(DirName(path)/arch_ids/'=id')
 
1093
    raise AssertionError, 'not regular file or directory: ' + path