1
# Copyright (C) 2004 Aaron Bentley
2
# <aaron.bentley@utoronto.ca>
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.
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.
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
27
__docformat__ = "restructuredtext"
28
__doc__ = "Native implementation of Arch functionality"
30
def cat_log(dir, revision):
31
"""Return a string containing the patchlog for a revision.
33
:param dir: The tree-root directory
35
:param revision: The revision of the log to get
38
return open(paths.tree_log(dir, arch.Revision(revision))).read()
42
"""Provide list-style access to Arch parameters."""
43
def __init__(self, dir=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)
51
def name_path(self, param_name):
52
"""Produce the path associated with the param name"""
53
return self.dir + "/" + param_name
55
def __getitem__(self, name):
56
"""Return the value of the parameter. For directories, this is another
59
:param name: The name of the parameter
61
:rtype: str or `ArchParams`
63
path = self.name_path(name)
64
if not os.access(path, os.R_OK):
66
if os.path.isdir(path):
67
return ArchParams(path)
69
return open(path).read()
71
def exists(self, name):
72
"""Determine whether a given parameter exists"""
73
return os.access(self.name_path(name), os.R_OK)
75
def value(self, name):
76
"""Get the value of the parameter, or None if it doesn't exist
78
:param name: The name of the parameter
80
:rtype: str, ArchParams, or NoneType
83
return self.__getitem__(name)
90
return os.listdir(self.dir).__iter__()
94
# Override standard cat_log, so automatic patchlog reading is faster
95
arch.backends.baz.cat_log = cat_log
98
def _tree_root_dir(path):
99
if os.path.exists(os.path.join(path, "{arch}")):
102
newpath = (path+"/..")
103
if not os.path.samefile(newpath, path):
104
return _tree_root_dir(newpath)
109
def is_arch_dir(path):
110
"""Determine whether the specified path is in an Arch tree
111
:param path: The path to examine
115
return _tree_root_dir(path) is not None
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)
126
def invoke_diff(orig_dir, orig_name, mod_dir, mod_name, diff_args=None):
127
"""Invoke diff on files using tla style
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
137
if orig_name is not None:
138
orig_filename = orig_dir+'/'+orig_name
139
orig_display = "orig"+orig_name[1:]
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:]
147
mod_filename = "/dev/null"
148
mod_display = "/dev/null"
150
if diff_args is None:
152
args = ["--binary", "-L", orig_display, "-L", mod_display]
153
args.extend(diff_args)
154
args.extend((orig_filename, mod_filename))
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:
160
raise BinaryFiles(orig_filename, mod_filename)
162
def invoke_patch(file, diff, out_file, reverse=False):
163
args = ["-f", "-s", "--posix", "--binary"]
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))
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,
180
out_file.write(output)
182
open(mine_path+".rej", "wb").write(
183
"Conflicts occured, diff3 conflict markers left in file.\n")
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.
192
hunk_prompt is not included.
193
:param value: The value to set everything to
195
self.all_types(value)
196
self.hunk_prompt = False
197
self.keep_pattern = None
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
205
self.hunk_prompt = active
206
self.hunk_confirm = confirm
207
self.hunk_display = display
210
def all_types(self, value):
211
"""Sets all types of change to True or False.
213
hunk_prompt is not included.
214
:param value: The value to set everything to
216
self.file_perms = value
217
self.file_contents = value
218
self.deletions = value
219
self.additions = value
223
def add_keep_file(self, new_file):
224
"""Adds a filename to the keep_pattern string.
226
:param new_file: The new file to add. (from tree root)
229
self.add_keep_pattern("^"+util.regex_escape(new_file)+"$")
231
def add_keep_pattern(self, new_pattern):
232
"""Adds a pattern to the keep_pattern string. Patterns are ORed.
234
:param new_pattern: The pattern to add
235
:type new_pattern: str
237
if self.keep_pattern is not None:
238
self.keep_pattern += "|"
240
self.keep_pattern = ""
241
self.keep_pattern += new_pattern
242
return self.keep_pattern
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
250
:param changeset: The changeset the file is in
251
:type changeset: `ChangesetMunger`
252
:param id: The inventory id of the file
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"
263
self.orig_name = None
264
self.orig_type = None
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"
276
self.init_attribs(changeset)
278
def init_attribs(self, changeset):
279
"""Uses the mod and orig info to determine which change files exist
281
:param changeset: The changeset containing the changes
282
:type changeset: `ChangesetMunger`
284
self.removed_file = None
285
self.orig_perms = None
289
if self.orig_type == "file":
290
self.removed_file = changeset.path_exists(self.orig_name,
291
"removed-files-archive")
293
self.old_perms = changeset.orig_dir_metadata.get(self.orig_name)
296
self.mod_perms = None
298
if self.mod_name and self.mod_type == "file":
299
self.new_file = changeset.path_exists(self.mod_name,
301
self.mod_perms = changeset.path_exists(self.mod_name, "patches",
303
self.orig_perms = changeset.path_exists(self.mod_name, "patches",
305
self.diff = changeset.path_exists(self.mod_name, "patches",
307
self.original = changeset.path_exists(self.mod_name, "patches",
309
self.modified = changeset.path_exists(self.mod_name, "patches",
312
elif self.mod_name and self.mod_type == "dir":
313
self.mod_perms = changeset.path_exists(self.mod_name, "patches",
315
self.orig_perms = changeset.path_exists(self.mod_name, "patches",
317
self.new_perms = changeset.mod_dir_metadata.get(self.mod_name)
320
def rename(self, old_name, changeset, new_file, topdir, extension=""):
321
"""Rename a changeset and return its new pathname.
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
329
:param extension: The file extension, if any
331
:return: The new full pathname of the files
336
tmp = changeset.path(new_file, topdir, extension)
337
os.rename(old_name, tmp)
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.
345
:param new_name: the new file name
347
:param changeset: the changeset this entry is part of
348
:type changeset: `ChangesetMunger`
350
self.new_file = self.rename(self.new_file, changeset, new_name,
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",
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
364
def rename_orig(self, new_name, changeset):
365
"""Change the original name of this entry. This undoes renames.
367
:param new_name: the new file name
369
:param changeset: the changeset this entry is part of
370
:type changeset: `ChangesetMunger`
372
self.new_file = self.rename(self.new_file, changeset, new_name,
374
self.orig_name = new_name
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)
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)
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()
395
def get_print_name(self):
396
if self.mod_name is not None:
399
return self.orig_name
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)
407
class ChangesetMunger:
408
"""An abstraction of a changeset that can edit it"""
409
def __init__(self, changeset):
410
self.changeset = str(changeset)
412
def parse_index(self, filename):
414
for line in open(self.changeset+filename):
415
(value,key) = line.split(' ')
416
index[key.rstrip('\n')] = value
419
def parse_dir_metadata(self, filename):
421
for line in open(self.changeset+filename):
423
index[data[2]] = int(data[1], 8)
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')
435
def remove_unique(self, mod_files_index, orig_files_index):
437
for (key,value) in mod_files_index.iteritems():
438
if not orig_files_index.has_key(key):
441
del mod_files_index[key]
442
return mod_files_index
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
449
def read_indices(self):
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")
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")
462
print "Old changeset: no dir-metatdata"
463
self.mod_dir_metadata = {}
464
self.orig_dir_metadata = {}
468
raise errors.InvalidChangeset(self.changeset)
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)
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)
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)
492
def munge(self, opts=None):
493
"""Munges this changeset according to the specified options.
495
:param opts: The options to munge with
496
:type opts: `MungeOpts`
497
:return: The number of entries that matched the keep pattern
502
if opts.keep_pattern:
503
keep_pattern = re.compile(opts.keep_pattern)
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)
515
if not opts.file_contents or opts.hunk_prompt:
517
if entry.diff is not None:
518
patch_trim(entry.diff, opts.hunk_display,
521
entry.delete_contents_files()
522
if not opts.file_perms:
523
entry.delete_perms_files()
525
if not opts.additions:
526
self.remove_additions()
528
if not opts.deletions:
529
self.remove_deletions()
532
self.copy_values(self.orig_files_index, self.mod_files_index)
536
def entry_matches_pattern(self, entry, pattern):
537
"""Determine whether any of an entry's names matches the pattern.
539
:param entry: The ChangesetEntry to check
540
:type entry: `ChangesetEntry`
541
:param pattern: The pattern to check
542
:type pattern: Compiled regex
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)
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)
554
def safe_remove(self, index, id):
555
if index.has_key(id):
559
def get_entries(self):
560
"""Returns a map of ChangesetEntries
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)
569
def remove_entry(self, entry):
571
Remove all files and index entries for an ChangesetEntry.
573
:param entry: The entry to remove
574
:type entry: `ChangesetEntry`
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)
582
def patchfile(self, id):
583
if not self.mod_files_index.has_key(id):
584
print "no key%s" % id
586
path = self.changeset+"/patches"+self.mod_files_index[id][1:]+".patch"
588
if os.access(path, os.R_OK):
594
def path(self, name, topdir, extension=""):
595
return self.changeset + "/" + topdir + name[1:] + extension
598
def path_exists(self, name, topdir, extension=""):
599
pth = self.path(name, topdir, extension)
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"])
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])
618
def patch_trim(filename, display_func, confirm_func):
619
"""Remove hunks from a patch according to confirm_func
621
:param filename: The name of the patch file
623
:param confirm_func: The function to use for selecting hunks
624
:type confirm_func: 1-parameter callable returning bool
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')):
635
if line.startswith("@@"):
636
if maybe_write_hunk(output, hunk, header, confirm_func):
641
if maybe_write_hunk(output, hunk, header, confirm_func):
644
if header is not None:
648
def maybe_write_hunk(output, hunk, header, confirm_func):
649
"""Writes a hunk to a file, if it passes the confirmation function.
651
:param output: The place to write confirmed hunks
653
:param hunk: The hunk to write
655
:param header: The hunk header (if this would be the first in file)
657
:param confirm_func: The function to test hunks with
658
:type confirm_func: 1-parameter callable that returns bool
661
if confirm_func(hunk):
662
if header is not None:
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://"):
679
raise errors.UnsupportedScheme(location)
681
def get_pfs(location):
682
type,location = get_pfs_type(location)
683
return type(location)
686
def __init__(self, location):
687
self.location = location
690
return os.path.join(self.location, path)
693
return file(self.full_path(path), "rb")
697
stat(self.full_path(path))
702
def spliturl(location):
703
loc = urlparse.urlparse(location)
705
netloc = loc[1].split(':')
708
port = int(netloc[1])
711
return (scheme, host, port, loc[2])
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)
720
def __init__(self, location):
721
(scheme, self.host, self.port, self.path) = spliturl(location)
722
assert (scheme in ["http", "https"])
724
self.ConnectionType = httplib.HTTPConnection
725
if self.port is None:
726
self.port = httplib.HTTP_PORT
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)
734
def absolute_path(self, path):
735
return "%s/%s" % (self.path, path)
739
self.connection.request("GET", self.absolute_path(path))
740
response = self.connection.getresponse()
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)
749
def exists(self, path):
751
self.connection.request("HEAD", self.absolute_path(path))
752
response = self.connection.getresponse()
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:
760
elif response.status == 404:
764
raise HttpError(response)
766
def list(self, path):
767
l_path = path.rstrip('/')+'/'+'.listing'
769
return self.get(l_path).read().splitlines()
771
if not e.response.status == 404:
773
raise Exception("No listing file for %s" % l_path)
777
self.name = "=meta-info/name"
779
def get_category_path(self, category):
780
return category.nonarch
782
def get_branch_path(self, branch):
783
return "%s/%s" % (self.get_category_path(branch.category),
786
def get_version_path(self, version):
787
return "%s/%s" % (self.get_branch_path(version.branch), version.nonarch)
789
def get_revision_path(self, revision):
790
return "%s/%s" % (self.get_version_path(revision.version),
791
str(revision.patchlevel))
793
def get_revision_file(self, revision, file):
794
return "%s/%s" % (self.get_revision_path(revision), file)
800
ArchPath.__init__(self)
803
"Hackerlab arch archive directory, format version 2.": ArchPath,
804
"Bazaar archive format 1 0": BazPath,
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))
817
archives[location] = PfsArchive(location, name)
818
return archives[location]
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)" % \
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)
837
def exists(self, revision):
838
path = self.path_generator.get_revision_file(revision, "log")
839
return self.pfs.exists(path)
843
params = ArchParams()
844
if not params.exists('=arch-cache'):
846
return params['=arch-cache'].strip()
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)
854
def cache_get(query):
856
return open(os.path.join(cache_path(), query), "rb")
863
def cache_get_revision(revision, rfile):
864
return cache_get(cache_revision_query(revision, rfile))
868
def __init__(self, origline, modline):
869
self.orig = origline[3:]
870
self.mod = modline[3:]
873
return "---%s\n+++%s" % (self.orig, self.mod)
877
def __init__(self, line):
885
def __init__(self, line):
892
class DiffAddLine(DiffLine):
893
def __init__(self, line):
894
DiffLine.__init__(self, line)
900
class DiffRemoveLine(DiffLine):
901
def __init__(self, line):
902
DiffLine.__init__(self, line)
908
def diff_classifier(iter):
910
if isinstance(line, arch.Chatter):
912
elif line.startswith("---"):
914
elif line.startswith("+++"):
915
yield DiffFilenames(origname, line)
916
elif line.startswith("@"):
918
elif line.startswith("+"):
919
yield DiffAddLine(line)
920
elif line.startswith("-"):
921
yield DiffRemoveLine(line)
922
elif line.startswith(" "):
927
# arch-tag: b082ccd9-db04-422a-94d6-fb7fedcaabf0