~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to fai/pylon/native.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
# 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