~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/pylon/native.py

  • Committer: Robert Collins
  • Date: 2005-09-14 11:27:20 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-20050914112720-c66a21de86eafa6e
trim fai cribbage

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
3
 
#
4
 
#    This program is free software; you can redistribute it and/or modify
5
 
#    it under the terms of the GNU General Public License as published by
6
 
#    the Free Software Foundation; either version 2 of the License, or
7
 
#    (at your option) any later version.
8
 
#
9
 
#    This program is distributed in the hope that it will be useful,
10
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
#    GNU General Public License for more details.
13
 
#
14
 
#    You should have received a copy of the GNU General Public License
15
 
#    along with this program; if not, write to the Free Software
16
 
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
 
 
18
 
import pybaz as arch
19
 
import paths
20
 
import os
21
 
import errors
22
 
import util
23
 
import re
24
 
import urlparse
25
 
import httplib
26
 
 
27
 
__docformat__ = "restructuredtext"
28
 
__doc__ = "Native implementation of Arch functionality"
29
 
 
30
 
def cat_log(dir, revision):
31
 
    """Return a string containing the patchlog for a revision.
32
 
 
33
 
    :param dir: The tree-root directory
34
 
    :type dir: str
35
 
    :param revision: The revision of the log to get
36
 
    :type revision: str
37
 
    """
38
 
    return open(paths.tree_log(dir, arch.Revision(revision))).read()
39
 
 
40
 
 
41
 
class ArchParams:
42
 
    """Provide list-style access to Arch parameters."""
43
 
    def __init__(self, dir=None):
44
 
        if dir is None:
45
 
            self.dir = os.path.expanduser("~/.arch-params")
46
 
            if not os.access(self.dir, os.R_OK):
47
 
                raise errors.NoArchParams(self.dir)
48
 
        else:
49
 
            self.dir = dir
50
 
 
51
 
    def name_path(self, param_name):
52
 
        """Produce the path associated with the param name"""
53
 
        return self.dir + "/" + param_name
54
 
 
55
 
    def __getitem__(self, name):
56
 
        """Return the value of the parameter.  For directories, this is another
57
 
        ArchParam instance.
58
 
 
59
 
        :param name: The name of the parameter
60
 
        :type name: str
61
 
        :rtype: str or `ArchParams`
62
 
        """
63
 
        path = self.name_path(name)
64
 
        if not os.access(path, os.R_OK):
65
 
            raise KeyError
66
 
        if os.path.isdir(path):
67
 
            return ArchParams(path)
68
 
        else:
69
 
            return open(path).read()
70
 
 
71
 
    def exists(self, name):
72
 
        """Determine whether a given parameter exists"""
73
 
        return os.access(self.name_path(name), os.R_OK)
74
 
 
75
 
    def value(self, name):
76
 
        """Get the value of the parameter, or None if it doesn't exist
77
 
 
78
 
        :param name: The name of the parameter
79
 
        :type name: str
80
 
        :rtype: str, ArchParams, or NoneType
81
 
        """
82
 
        try: 
83
 
            return self.__getitem__(name)
84
 
        except KeyError:
85
 
            return None
86
 
                   
87
 
    has_key = exists
88
 
    
89
 
    def __iter__(self):
90
 
        return os.listdir(self.dir).__iter__()
91
 
 
92
 
    iterkeys = __iter__
93
 
 
94
 
# Override standard cat_log, so automatic patchlog reading is faster
95
 
arch.backends.baz.cat_log = cat_log
96
 
 
97
 
 
98
 
def _tree_root_dir(path):
99
 
    if os.path.exists(os.path.join(path, "{arch}")):
100
 
        return path
101
 
    else:
102
 
        newpath = (path+"/..")
103
 
        if not os.path.samefile(newpath, path):
104
 
            return _tree_root_dir(newpath)
105
 
        else:
106
 
            return None
107
 
 
108
 
 
109
 
def is_arch_dir(path):
110
 
    """Determine whether the specified path is in an Arch tree
111
 
    :param path: The path to examine
112
 
    :type path: str
113
 
    :return: bool
114
 
    """
115
 
    return _tree_root_dir(path) is not None 
116
 
 
117
 
 
118
 
class BinaryFiles(Exception):
119
 
    def __init__(self, orig, mod):
120
 
        msg = "Binary files %s and %s differ" % (orig, mod)
121
 
        Exception.__init__(self, msg)
122
 
        self.orig = orig
123
 
        self.mod = mod
124
 
 
125
 
 
126
 
def invoke_diff(orig_dir, orig_name, mod_dir, mod_name, diff_args=None):
127
 
    """Invoke diff on files using tla style
128
 
 
129
 
    :orig_dir: The old arch tree
130
 
    :orig_name: The old relative filename
131
 
    :mod_dir: The new arch tree
132
 
    :mod_name: The new relative filename
133
 
    :diff_args: Optional diff parameters ("-u") by default
134
 
    :return: The diffs produced
135
 
    :rtype: str
136
 
    """
137
 
    if orig_name is not None:
138
 
        orig_filename = orig_dir+'/'+orig_name
139
 
        orig_display = "orig"+orig_name[1:]
140
 
    else:
141
 
        orig_filename = "/dev/null"
142
 
        orig_display = "/dev/null"
143
 
    if mod_name is not None:
144
 
        mod_filename = mod_dir+'/'+mod_name
145
 
        mod_display = "mod"+mod_name[1:]
146
 
    else:
147
 
        mod_filename = "/dev/null"
148
 
        mod_display = "/dev/null"
149
 
 
150
 
    if diff_args is None:
151
 
        diff_args = ("-u",)
152
 
    args = ["--binary", "-L", orig_display, "-L", mod_display]
153
 
    args.extend(diff_args)
154
 
    args.extend((orig_filename, mod_filename))
155
 
    try:
156
 
        return arch.util.exec_safe_stdout('diff', args, expected=(0, 1))
157
 
    except arch.errors.ExecProblem, e:
158
 
        if not e.proc.status == 2:
159
 
            raise
160
 
        raise BinaryFiles(orig_filename, mod_filename)
161
 
    
162
 
def invoke_patch(file, diff, out_file, reverse=False):
163
 
    args = ["-f", "-s", "--posix", "--binary"]
164
 
    if reverse:
165
 
        args.append("--reverse")
166
 
    args.extend(("-i", diff, "-o", out_file, file))
167
 
    status = arch.util.exec_safe_silent('patch', args, expected=(0, 1))
168
 
    return status
169
 
 
170
 
def invoke_diff3(out_file, mine_path, older_path, yours_path):
171
 
    def add_label(args, label):
172
 
        args.extend(("-L", label))
173
 
    args = ["-E", "--merge"]
174
 
    add_label(args, "TREE")
175
 
    add_label(args, "ANCESTOR")
176
 
    add_label(args, "MERGE-SOURCE")
177
 
    args.extend((mine_path, older_path, yours_path))
178
 
    (status, output) = arch.util.exec_safe_status_stdout('diff3', args, 
179
 
        expected=(0,1))
180
 
    out_file.write(output)
181
 
    if status == 1:
182
 
        open(mine_path+".rej", "wb").write(
183
 
            "Conflicts occured, diff3 conflict markers left in file.\n")
184
 
    return status
185
 
 
186
 
 
187
 
class MungeOpts:
188
 
    """A set of options for changeset munging"""
189
 
    def __init__(self, value=False):
190
 
        """Initializes, sets all types of change to True or False.
191
 
 
192
 
        hunk_prompt is not included.
193
 
        :param value: The value to set everything to
194
 
        :type value: bool"""
195
 
        self.all_types(value)
196
 
        self.hunk_prompt = False
197
 
        self.keep_pattern = None
198
 
 
199
 
    def set_hunk_prompt(self, display, confirm, active=True):
200
 
        """Initializes the hunk-prompting settings
201
 
        :param display: The function to use for displaying hunk lines
202
 
        :param confirm: The function to use for confirming hunks
203
 
        :param active: If true, use hunk prompting
204
 
        """
205
 
        self.hunk_prompt = active
206
 
        self.hunk_confirm = confirm
207
 
        self.hunk_display = display
208
 
 
209
 
 
210
 
    def all_types(self, value):
211
 
        """Sets all types of change to True or False.
212
 
 
213
 
        hunk_prompt is not included.
214
 
        :param value: The value to set everything to
215
 
        :type value: bool"""
216
 
        self.file_perms = value
217
 
        self.file_contents = value
218
 
        self.deletions = value
219
 
        self.additions = value
220
 
        self.renames = value
221
 
 
222
 
 
223
 
    def add_keep_file(self, new_file):
224
 
        """Adds a filename to the keep_pattern string.
225
 
 
226
 
        :param new_file: The new file to add.  (from tree root)
227
 
        :type new_file: str
228
 
        """
229
 
        self.add_keep_pattern("^"+util.regex_escape(new_file)+"$")
230
 
 
231
 
    def add_keep_pattern(self, new_pattern):
232
 
        """Adds a pattern to the keep_pattern string.  Patterns are ORed.
233
 
 
234
 
        :param new_pattern: The pattern to add
235
 
        :type new_pattern: str
236
 
        """
237
 
        if self.keep_pattern is not None:
238
 
            self.keep_pattern += "|"
239
 
        else: 
240
 
            self.keep_pattern = ""
241
 
        self.keep_pattern += new_pattern
242
 
        return self.keep_pattern
243
 
 
244
 
 
245
 
class ChangesetEntry:
246
 
    """Represents the changes performed to a file or directory by a changeset"""
247
 
    def __init__(self, changeset, id):
248
 
        """Initial set-up.  Sets id, orig_name, orig_type, mod_name, mod_type
249
 
        
250
 
        :param changeset: The changeset the file is in
251
 
        :type changeset: `ChangesetMunger`
252
 
        :param id: The inventory id of the file
253
 
        :type id: str
254
 
        """
255
 
        self.id = id
256
 
        if changeset.orig_files_index.has_key(id):
257
 
            self.orig_name = changeset.orig_files_index[id]
258
 
            self.orig_type = "file"
259
 
        elif changeset.orig_dirs_index.has_key(id):
260
 
            self.orig_name = changeset.orig_dirs_index[id]
261
 
            self.orig_type = "dir"
262
 
        else:
263
 
            self.orig_name = None
264
 
            self.orig_type = None
265
 
 
266
 
        if changeset.mod_files_index.has_key(id):
267
 
            self.mod_name = changeset.mod_files_index[id]
268
 
            self.mod_type = "file"
269
 
        elif changeset.mod_dirs_index.has_key(id):
270
 
            self.mod_name = changeset.mod_dirs_index[id]
271
 
            self.mod_type = "dir"
272
 
        else:
273
 
            self.mod_name = None
274
 
            self.mod_type = None
275
 
 
276
 
        self.init_attribs(changeset)
277
 
 
278
 
    def init_attribs(self, changeset):
279
 
        """Uses the mod and orig info to determine which change files exist
280
 
 
281
 
        :param changeset: The changeset containing the changes
282
 
        :type changeset: `ChangesetMunger`
283
 
        """
284
 
        self.removed_file = None
285
 
        self.orig_perms = None
286
 
        self.original = None
287
 
        self.modified = None
288
 
        if self.orig_name:
289
 
            if self.orig_type == "file":
290
 
                self.removed_file = changeset.path_exists(self.orig_name,
291
 
                                                      "removed-files-archive")
292
 
            else:
293
 
                self.old_perms = changeset.orig_dir_metadata.get(self.orig_name)
294
 
 
295
 
        self.new_file = None
296
 
        self.mod_perms = None
297
 
        self.diff = None
298
 
        if self.mod_name and self.mod_type == "file":
299
 
            self.new_file = changeset.path_exists(self.mod_name,
300
 
                                                      "new-files-archive")
301
 
            self.mod_perms = changeset.path_exists(self.mod_name, "patches",
302
 
                                                   ".meta-mod")
303
 
            self.orig_perms = changeset.path_exists(self.mod_name, "patches",
304
 
                                                   ".meta-orig")
305
 
            self.diff = changeset.path_exists(self.mod_name, "patches", 
306
 
                                              ".patch")
307
 
            self.original = changeset.path_exists(self.mod_name, "patches", 
308
 
                                              ".original")
309
 
            self.modified = changeset.path_exists(self.mod_name, "patches", 
310
 
                                              ".modified")
311
 
 
312
 
        elif self.mod_name and self.mod_type == "dir":
313
 
            self.mod_perms = changeset.path_exists(self.mod_name, "patches",
314
 
                                                   "/=dir-meta-mod")
315
 
            self.orig_perms = changeset.path_exists(self.mod_name, "patches",
316
 
                                                   "/=dir-meta-orig")
317
 
            self.new_perms = changeset.mod_dir_metadata.get(self.mod_name)
318
 
 
319
 
 
320
 
    def rename(self, old_name, changeset, new_file, topdir, extension=""):
321
 
        """Rename a changeset and return its new pathname.
322
 
 
323
 
        :param old_name: The old full pathname
324
 
        :type old_name: str or NoneType
325
 
        :param changeset: The changeset containing this entry
326
 
        :type changeset: `ChangesetMunger`
327
 
        :param topdir: The top directory containing the file
328
 
        :type topdir: str
329
 
        :param extension: The file extension, if any
330
 
        :type extension: str
331
 
        :return: The new full pathname of the files
332
 
        :rtype: str
333
 
        """
334
 
        if old_name is None:
335
 
            return None
336
 
        tmp = changeset.path(new_file, topdir, extension)
337
 
        os.rename(old_name, tmp)
338
 
        return tmp
339
 
        
340
 
 
341
 
    def rename_mod(self, new_name, changeset):
342
 
        """Change the modified name of this entry.  This undoes renames,
343
 
        but probably not the way you'd like.
344
 
 
345
 
        :param new_name: the new file name
346
 
        :type new_name: str
347
 
        :param changeset: the changeset this entry is part of
348
 
        :type changeset: `ChangesetMunger`
349
 
        """
350
 
        self.new_file = self.rename(self.new_file, changeset, new_name,
351
 
                                    "new-files-archive")
352
 
        self.mod_perms = self.rename(self.mod_perms, changeset, new_name, 
353
 
                                     "patches", ".meta-mod")
354
 
        self.orig_perms = self.rename(self.orig_perms, changeset, new_name, 
355
 
                                     "patches", ".meta-orig")
356
 
        self.diff = self.rename(self.diff, changeset, new_name, "patches",
357
 
                                ".patch")
358
 
        self.original = self.rename(self.original, changeset, new_name, 
359
 
                                    "patches", ".original")
360
 
        self.modified = self.rename(self.modified, changeset, new_name, 
361
 
                                    "patches", ".modified")
362
 
        self.mod_name = new_name
363
 
 
364
 
    def rename_orig(self, new_name, changeset):
365
 
        """Change the original name of this entry.  This undoes renames.
366
 
 
367
 
        :param new_name: the new file name
368
 
        :type new_name: str
369
 
        :param changeset: the changeset this entry is part of
370
 
        :type changeset: `ChangesetMunger`
371
 
        """
372
 
        self.new_file = self.rename(self.new_file, changeset, new_name,
373
 
                                    "new-files-archive")
374
 
        self.orig_name = new_name
375
 
 
376
 
    def delete_contents_files(self):
377
 
        """delete the contents change files for this entry"""
378
 
        if self.mod_name is not None:
379
 
            util.safe_unlink(self.original)
380
 
            util.safe_unlink(self.diff)
381
 
            util.safe_unlink(self.modified)
382
 
 
383
 
    def delete_perms_files(self):
384
 
        """Delete the permissions files for this entry"""
385
 
        util.safe_unlink(self.orig_perms)
386
 
        util.safe_unlink(self.mod_perms)
387
 
        
388
 
    def delete_files(self):
389
 
        """Delete all change files for this ID"""
390
 
        util.safe_unlink(self.removed_file)
391
 
        util.safe_unlink(self.new_file)
392
 
        self.delete_contents_files()
393
 
        self.delete_perms_files()
394
 
 
395
 
    def get_print_name(self):
396
 
        if self.mod_name is not None:
397
 
            return self.mod_name
398
 
        else:
399
 
            return self.orig_name
400
 
 
401
 
    def get_perm_change(self):
402
 
        orig = int(open(self.orig_perms).read().split()[1], 8)
403
 
        mod = int(open(self.mod_perms).read().split()[1], 8)
404
 
        return (orig, mod)
405
 
 
406
 
 
407
 
class ChangesetMunger:
408
 
    """An abstraction of a changeset that can edit it"""
409
 
    def __init__(self, changeset):
410
 
        self.changeset = str(changeset)
411
 
 
412
 
    def parse_index(self, filename):
413
 
        index = {}
414
 
        for line in open(self.changeset+filename):
415
 
            (value,key) = line.split(' ')
416
 
            index[key.rstrip('\n')] = value
417
 
        return index
418
 
 
419
 
    def parse_dir_metadata(self, filename):
420
 
        index = {}
421
 
        for line in open(self.changeset+filename):
422
 
            data = line.split()
423
 
            index[data[2]] = int(data[1], 8)
424
 
        return index
425
 
 
426
 
 
427
 
    def write_index(self, filename, index):
428
 
        os.unlink(self.changeset+filename)
429
 
        my_file = open(self.changeset+filename, 'w')
430
 
        for (key, value) in index.iteritems():
431
 
            my_file.write(' '.join((value, key))+'\n')
432
 
        my_file.flush()
433
 
        my_file.close()
434
 
 
435
 
    def remove_unique(self, mod_files_index, orig_files_index):
436
 
        keys = []
437
 
        for (key,value) in mod_files_index.iteritems():
438
 
            if not orig_files_index.has_key(key):
439
 
                keys.append(key)
440
 
        for key in keys:
441
 
            del mod_files_index[key]
442
 
        return mod_files_index
443
 
    
444
 
    def copy_values(self, mod_files_index, orig_files_index):
445
 
        for (key, value) in orig_files_index.iteritems():
446
 
            if mod_files_index.has_key(key):
447
 
                mod_files_index[key] = value
448
 
 
449
 
    def read_indices(self):
450
 
        try:
451
 
            self.orig_files_index = self.parse_index("/orig-files-index")
452
 
            self.mod_files_index = self.parse_index("/mod-files-index")
453
 
            self.orig_dirs_index = self.parse_index("/orig-dirs-index")
454
 
            self.mod_dirs_index = self.parse_index("/mod-dirs-index")
455
 
            try:
456
 
                self.orig_dir_metadata = \
457
 
                    self.parse_dir_metadata("/original-only-dir-metadata")
458
 
                self.mod_dir_metadata = \
459
 
                    self.parse_dir_metadata("/modified-only-dir-metadata")
460
 
            except IOError, e:
461
 
                if e.errno == 2:
462
 
                    print "Old changeset: no dir-metatdata"
463
 
                    self.mod_dir_metadata = {}
464
 
                    self.orig_dir_metadata = {}
465
 
                else:
466
 
                    raise
467
 
        except IOError:
468
 
            raise errors.InvalidChangeset(self.changeset)
469
 
            
470
 
 
471
 
    def write_indices(self):
472
 
        self.write_index("/orig-files-index", self.orig_files_index)
473
 
        self.write_index("/mod-files-index", self.mod_files_index)
474
 
        self.write_index("/mod-dirs-index", self.mod_dirs_index)
475
 
        self.write_index("/orig-dirs-index", self.orig_dirs_index)
476
 
 
477
 
    def remove_additions(self):
478
 
        for (root, dirs, files) in os.walk(self.changeset+"/new-files-archive"):
479
 
            for my_file in files:
480
 
                os.unlink("/".join((root,my_file)))
481
 
        self.remove_unique(self.mod_files_index, self.orig_files_index)
482
 
        self.remove_unique(self.mod_dirs_index, self.orig_dirs_index)
483
 
 
484
 
    def remove_deletions(self):
485
 
        for (root, dirs, files) in \
486
 
            os.walk(self.changeset+"/removed-files-archive"):
487
 
                for my_file in files:
488
 
                    os.unlink("/".join((root,my_file)))
489
 
        self.remove_unique(self.orig_files_index, self.mod_files_index)
490
 
        self.remove_unique(self.orig_dirs_index, self.mod_dirs_index)
491
 
 
492
 
    def munge(self, opts=None):
493
 
        """Munges this changeset according to the specified options.
494
 
 
495
 
        :param opts: The options to munge with
496
 
        :type opts: `MungeOpts`
497
 
        :return: The number of entries that matched the keep pattern
498
 
        """
499
 
        self.read_indices()
500
 
        matches = 0
501
 
 
502
 
        if opts.keep_pattern:
503
 
            keep_pattern = re.compile(opts.keep_pattern)
504
 
        else:
505
 
            keep_pattern = None
506
 
 
507
 
        for entry in self.get_entries().itervalues():
508
 
            if keep_pattern is not None and \
509
 
                not self.entry_matches_pattern(entry, keep_pattern):
510
 
                    self.remove_entry(entry)
511
 
                    continue
512
 
            else:
513
 
                matches+=1
514
 
                    
515
 
            if not opts.file_contents or opts.hunk_prompt:
516
 
                if opts.hunk_prompt:
517
 
                    if entry.diff is not None:
518
 
                        patch_trim(entry.diff, opts.hunk_display, 
519
 
                                   opts.hunk_confirm)
520
 
                else:
521
 
                    entry.delete_contents_files()
522
 
            if not opts.file_perms:
523
 
                entry.delete_perms_files()
524
 
                        
525
 
        if not opts.additions:
526
 
            self.remove_additions()
527
 
 
528
 
        if not opts.deletions:
529
 
            self.remove_deletions()
530
 
 
531
 
        if not opts.renames:
532
 
            self.copy_values(self.orig_files_index, self.mod_files_index)
533
 
        self.write_indices()
534
 
        return matches
535
 
 
536
 
    def entry_matches_pattern(self, entry, pattern):
537
 
        """Determine whether any of an entry's names matches the pattern.
538
 
        
539
 
        :param entry: The ChangesetEntry to check
540
 
        :type entry: `ChangesetEntry`
541
 
        :param pattern: The pattern to check
542
 
        :type pattern: Compiled regex
543
 
        """
544
 
        return (entry.orig_name is not None and \
545
 
            pattern.search(entry.orig_name) is not None) or \
546
 
            (entry.mod_name is not None and \
547
 
             pattern.search(entry.mod_name) is not None)
548
 
 
549
 
    def add_unique_entries(self, entries, index):
550
 
        for file_id in index.iterkeys():
551
 
            if not entries.has_key(file_id):
552
 
                entries[file_id] = ChangesetEntry(self, file_id)
553
 
 
554
 
    def safe_remove(self, index, id):
555
 
        if index.has_key(id):
556
 
            del index[id]
557
 
        
558
 
 
559
 
    def get_entries(self):
560
 
        """Returns a map of ChangesetEntries
561
 
        """
562
 
        entries = {}
563
 
        self.add_unique_entries(entries, self.orig_files_index)
564
 
        self.add_unique_entries(entries, self.orig_dirs_index)
565
 
        self.add_unique_entries(entries, self.mod_files_index)
566
 
        self.add_unique_entries(entries, self.mod_dirs_index)
567
 
        return entries
568
 
 
569
 
    def remove_entry(self, entry):
570
 
        """
571
 
        Remove all files and index entries for an ChangesetEntry.
572
 
 
573
 
        :param entry: The entry to remove
574
 
        :type entry: `ChangesetEntry`
575
 
        """
576
 
        entry.delete_files()
577
 
        self.safe_remove(self.orig_files_index, entry.id)
578
 
        self.safe_remove(self.orig_dirs_index, entry.id)
579
 
        self.safe_remove(self.mod_files_index, entry.id)
580
 
        self.safe_remove(self.mod_dirs_index, entry.id)
581
 
 
582
 
    def patchfile(self, id):
583
 
        if not self.mod_files_index.has_key(id):
584
 
            print "no key%s" % id
585
 
            return None
586
 
        path = self.changeset+"/patches"+self.mod_files_index[id][1:]+".patch"
587
 
 
588
 
        if os.access(path, os.R_OK):
589
 
            return path
590
 
        else:
591
 
            return None
592
 
 
593
 
 
594
 
    def path(self, name, topdir, extension=""):
595
 
        return self.changeset + "/" + topdir + name[1:] + extension
596
 
        
597
 
 
598
 
    def path_exists(self, name, topdir, extension=""):
599
 
        pth = self.path(name, topdir, extension)
600
 
        try:
601
 
            os.lstat(pth)
602
 
            return pth
603
 
        except:
604
 
            return None
605
 
 
606
 
    def files_to_ids(self, filenames, type="mod"):
607
 
        f_idx = misc.invert_dict(self.__dict__[type+"_files_index"])
608
 
        d_idx = misc.invert_dict(self.__dict__[type+"_dirs_index"])
609
 
        idlist = []
610
 
        for my_file in filenames:
611
 
            if f_idx.has_key(my_file):
612
 
                idlist.append(f_idx[my_file])
613
 
            elif d_idx.has_key(my_file):
614
 
                idlist.append(d_idx[my_file])
615
 
        return idlist
616
 
 
617
 
 
618
 
def patch_trim(filename, display_func, confirm_func):
619
 
    """Remove hunks from a patch according to confirm_func
620
 
    
621
 
    :param filename: The name of the patch file
622
 
    :type filename: str
623
 
    :param confirm_func: The function to use for selecting hunks
624
 
    :type confirm_func: 1-parameter callable returning bool
625
 
    """
626
 
    source = open(filename)
627
 
    output = util.NewFileVersion(filename)
628
 
    header = source.next()
629
 
    header += source.next()
630
 
    for line in diff_classifier(header.split('\n')):
631
 
        display_func(line)
632
 
    
633
 
    hunk = None
634
 
    for line in source:
635
 
        if line.startswith("@@"):
636
 
            if maybe_write_hunk(output, hunk, header, confirm_func):
637
 
                header = None
638
 
            hunk = ""
639
 
        hunk += line
640
 
 
641
 
    if maybe_write_hunk(output, hunk, header, confirm_func):
642
 
        header = None
643
 
    output.commit()
644
 
    if header is not None:
645
 
        os.unlink(filename)
646
 
 
647
 
 
648
 
def maybe_write_hunk(output, hunk, header, confirm_func):
649
 
    """Writes a hunk to a file, if it passes the confirmation function.
650
 
 
651
 
    :param output: The place to write confirmed hunks
652
 
    :type output: file
653
 
    :param hunk: The hunk to write
654
 
    :type hunk: str
655
 
    :param header: The hunk header (if this would be the first in file)
656
 
    :type header: str
657
 
    :param confirm_func: The function to test hunks with
658
 
    :type confirm_func: 1-parameter callable that returns bool
659
 
    """
660
 
    if hunk is not None:
661
 
        if confirm_func(hunk):
662
 
            if header is not None:
663
 
                output.write(header)
664
 
            output.write(hunk)
665
 
            return True
666
 
    return False
667
 
 
668
 
 
669
 
def get_pfs_type(location):
670
 
    if location.startswith("cached:"):
671
 
        location=location[len("cached:"):]
672
 
    if location.startswith("/"):
673
 
        return PfsFilesystem, location
674
 
    elif location.startswith("http://"):
675
 
        return PfsHttp, location
676
 
    elif location.startswith("https://"):
677
 
        return PfsHttp
678
 
    else:
679
 
        raise errors.UnsupportedScheme(location)
680
 
 
681
 
def get_pfs(location):
682
 
    type,location = get_pfs_type(location)
683
 
    return type(location)
684
 
 
685
 
class PfsFilesystem:
686
 
    def __init__(self, location):
687
 
        self.location = location
688
 
 
689
 
    def full_path(path):
690
 
        return os.path.join(self.location, path)
691
 
 
692
 
    def get(path):
693
 
        return file(self.full_path(path), "rb")
694
 
 
695
 
    def exists(path):
696
 
        try:
697
 
            stat(self.full_path(path))
698
 
            return True
699
 
        except:
700
 
            return False
701
 
 
702
 
def spliturl(location):
703
 
    loc = urlparse.urlparse(location)
704
 
    scheme = loc[0]
705
 
    netloc = loc[1].split(':')
706
 
    host = netloc[0]
707
 
    if len(netloc) > 1:
708
 
        port = int(netloc[1])
709
 
    else:
710
 
        port = None
711
 
    return (scheme, host, port, loc[2])
712
 
 
713
 
class HttpError(Exception):
714
 
    def __init__(self, response):
715
 
        self.response = response
716
 
        msg = "Unexpected status: %s" % self.response.reason
717
 
        Exception.__init__(self, msg)
718
 
 
719
 
class PfsHttp:
720
 
    def __init__(self, location):
721
 
        (scheme, self.host, self.port, self.path) = spliturl(location)
722
 
        assert (scheme in ["http", "https"])
723
 
        if scheme == "http":
724
 
            self.ConnectionType = httplib.HTTPConnection
725
 
            if self.port is None:
726
 
                self.port = httplib.HTTP_PORT
727
 
        else:
728
 
            self.ConnectionType = httplib.HTTPSConnection
729
 
            if self.port is None:
730
 
                self.port = httplib.HTTPS_PORT
731
 
        self.path = self.path.rstrip('/')
732
 
        self.connection = self.ConnectionType(self.host, self.port)
733
 
 
734
 
    def absolute_path(self, path):
735
 
        return "%s/%s" % (self.path, path)
736
 
        
737
 
    def get(self, path):
738
 
        try:
739
 
            self.connection.request("GET", self.absolute_path(path))
740
 
            response = self.connection.getresponse()
741
 
        except:
742
 
            self.connection = self.ConnectionType(self.host, self.port)        
743
 
            self.connection.request("GET", self.absolute_path(path))
744
 
            response = self.connection.getresponse()
745
 
        if response.status != 200:
746
 
            raise HttpError(response)
747
 
        return response
748
 
 
749
 
    def exists(self, path):
750
 
        try:
751
 
            self.connection.request("HEAD", self.absolute_path(path))
752
 
            response = self.connection.getresponse()
753
 
        except Exception, e:
754
 
            self.connection = self.ConnectionType(self.host, self.port)        
755
 
            self.connection.request("HEAD", self.absolute_path(path))
756
 
            response = self.connection.getresponse()
757
 
        if response.status == 200:
758
 
            response.read()
759
 
            return True
760
 
        elif response.status == 404:
761
 
            response.read()
762
 
            return False
763
 
        else:
764
 
            raise HttpError(response)
765
 
 
766
 
    def list(self, path):
767
 
        l_path = path.rstrip('/')+'/'+'.listing'
768
 
        try:
769
 
            return self.get(l_path).read().splitlines()
770
 
        except HttpError, e:
771
 
            if not e.response.status == 404:
772
 
                raise
773
 
            raise Exception("No listing file for %s" % l_path)
774
 
 
775
 
class ArchPath:
776
 
    def __init__(self):
777
 
        self.name = "=meta-info/name"
778
 
 
779
 
    def get_category_path(self, category):
780
 
        return category.nonarch
781
 
 
782
 
    def get_branch_path(self, branch):
783
 
        return "%s/%s" % (self.get_category_path(branch.category), 
784
 
                          branch.nonarch)
785
 
 
786
 
    def get_version_path(self, version):
787
 
        return "%s/%s" % (self.get_branch_path(version.branch), version.nonarch)
788
 
 
789
 
    def get_revision_path(self, revision):
790
 
        return "%s/%s" % (self.get_version_path(revision.version),
791
 
                          str(revision.patchlevel))
792
 
 
793
 
    def get_revision_file(self, revision, file):
794
 
        return "%s/%s" % (self.get_revision_path(revision), file)
795
 
        
796
 
        
797
 
        
798
 
class BazPath:
799
 
     def __init__(self):
800
 
        ArchPath.__init__(self)
801
 
 
802
 
path_generator = {
803
 
"Hackerlab arch archive directory, format version 2.": ArchPath,
804
 
"Bazaar archive format 1 0": BazPath,
805
 
}
806
 
 
807
 
archives = {}
808
 
 
809
 
def get_pfs_archive(location, name):
810
 
    archive = archives.get(location)
811
 
    if archive is not None:
812
 
        if archive.name != name:
813
 
            raise Exception("Archive has wrong name %s (expected %s)" \
814
 
                % (archive.name, name))
815
 
        return archive
816
 
    else:
817
 
        archives[location] = PfsArchive(location, name)
818
 
        return archives[location]
819
 
 
820
 
 
821
 
class PfsArchive:
822
 
    def __init__(self, location, name):
823
 
        self.pfs = get_pfs(location)
824
 
        self.archive_type = self.pfs.get(".archive-version").read().strip()
825
 
        self.path_generator = path_generator[self.archive_type]()
826
 
        self.name = self.pfs.get(self.path_generator.name).read().strip()
827
 
        if name is not None and self.name != name:
828
 
            raise Exception("Archive has wrong name %s (expected %s)" % \
829
 
                (self.name, name))
830
 
 
831
 
    def get_patch(self, revision):
832
 
        myfile = revision.nonarch+".patches.tar.gz"
833
 
        path = self.path_generator.get_revision_file(revision, myfile)
834
 
        return self.pfs.get(path)
835
 
            
836
 
 
837
 
    def exists(self, revision):
838
 
        path = self.path_generator.get_revision_file(revision, "log")
839
 
        return self.pfs.exists(path)
840
 
 
841
 
 
842
 
def cache_path():
843
 
    params = ArchParams()
844
 
    if not params.exists('=arch-cache'):
845
 
        return None
846
 
    return params['=arch-cache'].strip()
847
 
 
848
 
def cache_revision_query(revision, rfile=None):
849
 
    path = os.path.join("archives/",str(revision))
850
 
    if rfile is not None:
851
 
        path = os.path.join(path, rfile)
852
 
    return path
853
 
 
854
 
def cache_get(query):
855
 
    try:
856
 
        return open(os.path.join(cache_path(), query), "rb")
857
 
    except IOError, e:
858
 
        if e.errno == 2:
859
 
            return None
860
 
        else:
861
 
            raise
862
 
 
863
 
def cache_get_revision(revision, rfile):
864
 
    return cache_get(cache_revision_query(revision, rfile))
865
 
 
866
 
 
867
 
class DiffFilenames:
868
 
    def __init__(self, origline, modline):
869
 
        self.orig = origline[3:]
870
 
        self.mod = modline[3:]
871
 
 
872
 
    def __str__(self):
873
 
        return "---%s\n+++%s" % (self.orig, self.mod)
874
 
 
875
 
 
876
 
class DiffHunk:
877
 
    def __init__(self, line):
878
 
        self.line=line
879
 
 
880
 
    def __str__(self):
881
 
        return self.line
882
 
 
883
 
 
884
 
class DiffLine:
885
 
    def __init__(self, line):
886
 
        self.line = line[1:]
887
 
 
888
 
    def __str__(self):
889
 
        return " "+self.line
890
 
 
891
 
 
892
 
class DiffAddLine(DiffLine):
893
 
    def __init__(self, line):
894
 
        DiffLine.__init__(self, line)
895
 
    
896
 
    def __str__(self):
897
 
        return "+"+self.line
898
 
 
899
 
 
900
 
class DiffRemoveLine(DiffLine):
901
 
    def __init__(self, line):
902
 
        DiffLine.__init__(self, line)
903
 
    
904
 
    def __str__(self):
905
 
        return "-"+self.line
906
 
 
907
 
 
908
 
def diff_classifier(iter):
909
 
    for line in iter:
910
 
        if isinstance(line, arch.Chatter):
911
 
            yield line
912
 
        elif line.startswith("---"):
913
 
            origname=line
914
 
        elif line.startswith("+++"):
915
 
            yield DiffFilenames(origname, line)
916
 
        elif line.startswith("@"):
917
 
            yield DiffHunk(line)
918
 
        elif line.startswith("+"):
919
 
            yield DiffAddLine(line)
920
 
        elif line.startswith("-"):
921
 
            yield DiffRemoveLine(line)
922
 
        elif line.startswith(" "):
923
 
            yield DiffLine(line)
924
 
        else:
925
 
            yield line
926
 
 
927
 
# arch-tag: b082ccd9-db04-422a-94d6-fb7fedcaabf0