162
174
class History (object):
165
self._change_cache = None
167
self._lock = threading.RLock()
170
def from_branch(cls, branch, name=None):
175
"""Decorate a branch to provide information for rendering.
177
History objects are expected to be short lived -- when serving a request
178
for a particular branch, open it, read-lock it, wrap a History object
179
around it, serve the request, throw the History object away, unlock the
180
branch and throw it away.
182
:ivar _file_change_cache: xx
185
def __init__(self, branch, whole_history_data_cache):
186
assert branch.is_locked(), (
187
"Can only construct a History object with a read-locked branch.")
188
self._file_change_cache = None
173
189
self._branch = branch
174
self._history = branch.revision_history()
175
self._last_revid = self._history[-1]
176
self._revision_graph = branch.repository.get_revision_graph(self._last_revid)
179
name = self._branch.nick
181
self.log = logging.getLogger('loggerhead.%s' % (name,))
183
self._full_history = []
184
self._revision_info = {}
185
self._revno_revid = {}
186
self._merge_sort = bzrlib.tsort.merge_sort(self._revision_graph, self._last_revid, generate_revno=True)
188
for (seq, revid, merge_depth, revno, end_of_merge) in self._merge_sort:
189
self._full_history.append(revid)
190
revno_str = '.'.join(str(n) for n in revno)
191
self._revno_revid[revno_str] = revid
192
self._revision_info[revid] = (seq, revid, merge_depth, revno_str, end_of_merge)
197
self._where_merged = {}
198
for revid in self._revision_graph.keys():
199
if not revid in self._full_history:
201
for parent in self._revision_graph[revid]:
202
self._where_merged.setdefault(parent, set()).add(revid)
204
self.log.info('built revision graph cache: %r secs' % (time.time() - z,))
208
def from_folder(cls, path, name=None):
209
b = bzrlib.branch.Branch.open(path)
210
return cls.from_branch(b, name)
213
def out_of_date(self):
214
if self._branch.revision_history()[-1] != self._last_revid:
218
def use_cache(self, cache):
219
self._change_cache = cache
221
def use_search_index(self, index):
226
# called when a new history object needs to be created, because the
227
# branch history has changed. we need to immediately close and stop
228
# using our caches, because a new history object will be created to
229
# replace us, using the same cache files.
230
# (may also be called during server shutdown.)
231
if self._change_cache is not None:
232
self._change_cache.close()
233
self._change_cache = None
234
if self._index is not None:
238
def flush_cache(self):
239
if self._change_cache is None:
241
self._change_cache.flush()
243
def check_rebuild(self):
244
if self._change_cache is not None:
245
self._change_cache.check_rebuild()
246
if self._index is not None:
247
self._index.check_rebuild()
249
last_revid = property(lambda self: self._last_revid, None, None)
251
count = property(lambda self: self._count, None, None)
190
self.log = logging.getLogger('loggerhead.%s' % (branch.nick,))
192
self.last_revid = branch.last_revision()
194
whole_history_data = whole_history_data_cache.get(self.last_revid)
195
if whole_history_data is None:
196
whole_history_data = compute_whole_history_data(branch)
197
whole_history_data_cache[self.last_revid] = whole_history_data
199
(self._revision_graph, self._full_history, self._revision_info,
200
self._revno_revid, self._merge_sort, self._where_merged
201
) = whole_history_data
203
def use_file_cache(self, cache):
204
self._file_change_cache = cache
207
def has_revisions(self):
208
return not bzrlib.revision.is_null(self.last_revid)
254
210
def get_config(self):
255
211
return self._branch.get_config()
258
def get_revision(self, revid):
259
return self._branch.repository.get_revision(revid)
261
213
def get_revno(self, revid):
262
214
if revid not in self._revision_info:
265
217
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
268
def get_sequence(self, revid):
269
seq, revid, merge_depth, revno_str, end_of_merge = self._revision_info[revid]
272
def get_revision_history(self):
273
return self._full_history
275
def get_revid_sequence(self, revid_list, revid):
277
given a list of revision ids, return the sequence # of this revid in
286
def get_revids_from(self, revid_list, revid):
288
given a list of revision ids, yield revisions in graph order,
289
starting from revid. the list can be None if you just want to travel
290
across all revisions.
293
if (revid_list is None) or (revid in revid_list):
220
def get_revids_from(self, revid_list, start_revid):
222
Yield the mainline (wrt start_revid) revisions that merged each
225
if revid_list is None:
226
revid_list = self._full_history
227
revid_set = set(revid_list)
229
def introduced_revisions(revid):
231
seq, revid, md, revno, end_of_merge = self._revision_info[revid]
233
while i < len(self._merge_sort) and self._merge_sort[i][2] > md:
234
r.add(self._merge_sort[i][1])
238
if bzrlib.revision.is_null(revid):
240
if introduced_revisions(revid) & revid_set:
295
if not self._revision_graph.has_key(revid):
297
242
parents = self._revision_graph[revid]
298
243
if len(parents) == 0:
300
245
revid = parents[0]
303
247
def get_short_revision_history_by_fileid(self, file_id):
304
248
# wow. is this really the only way we can get this list? by
305
249
# man-handling the weave store directly? :-0
306
250
# FIXME: would be awesome if we could get, for a folder, the list of
307
251
# revisions where items within that folder changed.
308
w = self._branch.repository.weave_store.get_weave(file_id, self._branch.repository.get_transaction())
309
w_revids = w.versions()
310
revids = [r for r in self._full_history if r in w_revids]
252
possible_keys = [(file_id, revid) for revid in self._full_history]
253
existing_keys = self._branch.repository.texts.get_parent_map(possible_keys)
254
return [revid for _, revid in existing_keys.iterkeys()]
314
256
def get_revision_history_since(self, revid_list, date):
315
257
# if a user asks for revisions starting at 01-sep, they mean inclusive,
316
258
# so start at midnight on 02-sep.
406
319
# if a "revid" is actually a dotted revno, convert it to a revid
407
320
if revid is None:
323
return self.last_revid
409
324
if self.revno_re.match(revid):
410
325
revid = self._revno_revid[revid]
414
328
def get_file_view(self, revid, file_id):
416
Given an optional revid and optional path, return a (revlist, revid)
417
for navigation through the current scope: from the revid (or the
418
latest revision) back to the original revision.
330
Given a revid and optional path, return a (revlist, revid) for
331
navigation through the current scope: from the revid (or the latest
332
revision) back to the original revision.
420
334
If file_id is None, the entire revision history is the list scope.
421
If revid is None, the latest revision is used.
423
336
if revid is None:
424
revid = self._last_revid
337
revid = self.last_revid
425
338
if file_id is not None:
426
# since revid is 'start_revid', possibly should start the path tracing from revid... FIXME
427
inv = self._branch.repository.get_revision_inventory(revid)
339
# since revid is 'start_revid', possibly should start the path
340
# tracing from revid... FIXME
428
341
revlist = list(self.get_short_revision_history_by_fileid(file_id))
429
342
revlist = list(self.get_revids_from(revlist, revid))
431
344
revlist = list(self.get_revids_from(None, revid))
434
return revlist, revid
437
347
def get_view(self, revid, start_revid, file_id, query=None):
439
349
use the URL parameters (revid, start_revid, file_id, and query) to
440
350
determine the revision list we're viewing (start_revid, file_id, query)
441
351
and where we are in it (revid).
443
if a query is given, we're viewing query results.
444
if a file_id is given, we're viewing revisions for a specific file.
445
if a start_revid is given, we're viewing the branch from a
446
specific revision up the tree.
447
(these may be combined to view revisions for a specific file, from
448
a specific revision, with a specific search query.)
450
returns a new (revid, start_revid, revid_list, scan_list) where:
353
- if a query is given, we're viewing query results.
354
- if a file_id is given, we're viewing revisions for a specific
356
- if a start_revid is given, we're viewing the branch from a
357
specific revision up the tree.
359
these may be combined to view revisions for a specific file, from
360
a specific revision, with a specific search query.
362
returns a new (revid, start_revid, revid_list) where:
452
364
- revid: current position within the view
453
365
- start_revid: starting revision of this view
454
366
- revid_list: list of revision ids for this view
456
368
file_id and query are never changed so aren't returned, but they may
457
369
contain vital context for future url navigation.
371
if start_revid is None:
372
start_revid = self.last_revid
459
374
if query is None:
460
revid_list, start_revid = self.get_file_view(start_revid, file_id)
375
revid_list = self.get_file_view(start_revid, file_id)
461
376
if revid is None:
462
377
revid = start_revid
463
378
if revid not in revid_list:
464
379
# if the given revid is not in the revlist, use a revlist that
465
380
# starts at the given revid.
466
revid_list, start_revid = self.get_file_view(revid, file_id)
381
revid_list = self.get_file_view(revid, file_id)
467
383
return revid, start_revid, revid_list
469
385
# potentially limit the search
470
if (start_revid is not None) or (file_id is not None):
471
revid_list, start_revid = self.get_file_view(start_revid, file_id)
386
if file_id is not None:
387
revid_list = self.get_file_view(start_revid, file_id)
473
389
revid_list = None
475
revid_list = self.get_search_revid_list(query, revid_list)
476
if len(revid_list) > 0:
390
revid_list = search.search_revisions(self._branch, query)
391
if revid_list and len(revid_list) > 0:
477
392
if revid not in revid_list:
478
393
revid = revid_list[0]
479
394
return revid, start_revid, revid_list
396
# XXX: This should return a message saying that the search could
397
# not be completed due to either missing the plugin or missing a
482
399
return None, None, []
485
401
def get_inventory(self, revid):
486
402
return self._branch.repository.get_revision_inventory(revid)
489
404
def get_path(self, revid, file_id):
490
405
if (file_id is None) or (file_id == ''):
561
475
p_changes = self.get_changes(list(fetch_set))
562
476
p_change_dict = dict([(c.revid, c) for c in p_changes])
563
477
for change in changes:
478
# arch-converted branches may not have merged branch info :(
564
479
for p in change.parents:
565
p.branch_nick = p_change_dict[p.revid].branch_nick
480
if p.revid in p_change_dict:
481
p.branch_nick = p_change_dict[p.revid].branch_nick
483
p.branch_nick = '(missing)'
566
484
for p in change.merge_points:
567
p.branch_nick = p_change_dict[p.revid].branch_nick
570
def get_changes(self, revid_list, get_diffs=False):
571
if self._change_cache is None:
572
changes = self.get_changes_uncached(revid_list, get_diffs)
574
changes = self._change_cache.get_changes(revid_list, get_diffs)
485
if p.revid in p_change_dict:
486
p.branch_nick = p_change_dict[p.revid].branch_nick
488
p.branch_nick = '(missing)'
490
def get_changes(self, revid_list):
491
"""Return a list of changes objects for the given revids.
493
Revisions not present and NULL_REVISION will be ignored.
495
changes = self.get_changes_uncached(revid_list)
496
if len(changes) == 0:
578
499
# some data needs to be recalculated each time, because it may
579
500
# change as new revisions are added.
580
for i in xrange(len(revid_list)):
581
revid = revid_list[i]
583
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(revid))
501
for change in changes:
502
merge_revids = self.simplify_merge_point_list(self.get_merge_point_list(change.revid))
584
503
change.merge_points = [util.Container(revid=r, revno=self.get_revno(r)) for r in merge_revids]
504
if len(change.parents) > 0:
505
change.parents = [util.Container(revid=r,
506
revno=self.get_revno(r)) for r in change.parents]
507
change.revno = self.get_revno(change.revid)
510
for change in changes:
511
change.parity = parity
588
# alright, let's profile this sucka.
589
def _get_changes_profiled(self, revid_list, get_diffs=False):
590
from loggerhead.lsprof import profile
592
ret, stats = profile(self.get_changes_uncached, revid_list, get_diffs)
595
cPickle.dump(stats, open('lsprof.stats', 'w'), 2)
596
self.log.info('lsprof complete!')
516
def get_changes_uncached(self, revid_list):
517
# FIXME: deprecated method in getting a null revision
518
revid_list = filter(lambda revid: not bzrlib.revision.is_null(revid),
520
parent_map = self._branch.repository.get_graph().get_parent_map(revid_list)
521
# We need to return the answer in the same order as the input,
523
present_revids = [revid for revid in revid_list
524
if revid in parent_map]
525
rev_list = self._branch.repository.get_revisions(present_revids)
527
return [self._change_from_revision(rev) for rev in rev_list]
599
529
def _get_deltas_for_revisions_with_trees(self, revisions):
600
"""Produce a generator of revision deltas.
530
"""Produce a list of revision deltas.
602
532
Note that the input is a sequence of REVISIONS, not revision_ids.
603
533
Trees will be held in memory until the generator exits.
604
534
Each delta is relative to the revision's lefthand predecessor.
535
(This is copied from bzrlib.)
606
537
required_trees = set()
607
538
for revision in revisions:
608
required_trees.add(revision.revision_id)
609
required_trees.update(revision.parent_ids[:1])
610
trees = dict((t.get_revision_id(), t) for
539
required_trees.add(revision.revid)
540
required_trees.update([p.revid for p in revision.parents[:1]])
541
trees = dict((t.get_revision_id(), t) for
611
542
t in self._branch.repository.revision_trees(required_trees))
613
544
self._branch.repository.lock_read()
615
546
for revision in revisions:
616
if not revision.parent_ids:
617
old_tree = self._branch.repository.revision_tree(None)
547
if not revision.parents:
548
old_tree = self._branch.repository.revision_tree(
549
bzrlib.revision.NULL_REVISION)
619
old_tree = trees[revision.parent_ids[0]]
620
tree = trees[revision.revision_id]
621
ret.append((tree, old_tree, tree.changes_from(old_tree)))
551
old_tree = trees[revision.parents[0].revid]
552
tree = trees[revision.revid]
553
ret.append(tree.changes_from(old_tree))
624
556
self._branch.repository.unlock()
627
@with_bzrlib_read_lock
628
def get_changes_uncached(self, revid_list, get_diffs=False):
630
rev_list = self._branch.repository.get_revisions(revid_list)
631
except (KeyError, bzrlib.errors.NoSuchRevision):
634
delta_list = self._get_deltas_for_revisions_with_trees(rev_list)
635
combined_list = zip(rev_list, delta_list)
638
for rev, (new_tree, old_tree, delta) in combined_list:
639
commit_time = datetime.datetime.fromtimestamp(rev.timestamp)
641
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in rev.parent_ids]
643
if len(parents) == 0:
558
def _change_from_revision(self, revision):
560
Given a bzrlib Revision, return a processed "change" for use in
563
commit_time = datetime.datetime.fromtimestamp(revision.timestamp)
565
parents = [util.Container(revid=r, revno=self.get_revno(r)) for r in revision.parent_ids]
567
message, short_message = clean_message(revision.message)
570
'revid': revision.revision_id,
572
'author': revision.get_apparent_author(),
573
'branch_nick': revision.properties.get('branch-nick', None),
574
'short_comment': short_message,
575
'comment': revision.message,
576
'comment_clean': [util.html_clean(s) for s in message],
577
'parents': revision.parent_ids,
579
return util.Container(entry)
581
def get_file_changes_uncached(self, entries):
582
delta_list = self._get_deltas_for_revisions_with_trees(entries)
584
return [self.parse_delta(delta) for delta in delta_list]
586
def get_file_changes(self, entries):
587
if self._file_change_cache is None:
588
return self.get_file_changes_uncached(entries)
590
return self._file_change_cache.get_file_changes(entries)
592
def add_changes(self, entries):
593
changes_list = self.get_file_changes(entries)
595
for entry, changes in zip(entries, changes_list):
596
entry.changes = changes
598
def get_change_with_diff(self, revid, compare_revid=None):
599
change = self.get_changes([revid])[0]
601
if compare_revid is None:
603
compare_revid = change.parents[0].revid
646
left_parent = rev.parent_ids[0]
648
message = rev.message.splitlines()
649
if len(message) == 1:
650
# robey-style 1-line long message
651
message = textwrap.wrap(message[0])
653
# make short form of commit message
654
short_message = message[0]
655
if len(short_message) > 60:
656
short_message = short_message[:60] + '...'
659
'revid': rev.revision_id,
660
'revno': self.get_revno(rev.revision_id),
662
'author': rev.committer,
663
'branch_nick': rev.properties.get('branch-nick', None),
664
'short_comment': short_message,
665
'comment': rev.message,
666
'comment_clean': [util.html_clean(s) for s in message],
668
'changes': self.parse_delta(delta, get_diffs, old_tree, new_tree),
670
entries.append(util.Container(entry))
605
compare_revid = 'null:'
607
rev_tree1 = self._branch.repository.revision_tree(compare_revid)
608
rev_tree2 = self._branch.repository.revision_tree(revid)
609
delta = rev_tree2.changes_from(rev_tree1)
611
change.changes = self.parse_delta(delta)
612
change.changes.modified = self._parse_diffs(rev_tree1, rev_tree2, delta)
675
616
def get_file(self, file_id, revid):
676
"returns (filename, data)"
677
inv_entry = self.get_inventory(revid)[file_id]
617
"returns (path, filename, data)"
618
inv = self.get_inventory(revid)
619
inv_entry = inv[file_id]
678
620
rev_tree = self._branch.repository.revision_tree(inv_entry.revision)
679
return inv_entry.name, rev_tree.get_file_text(file_id)
682
def parse_delta(self, delta, get_diffs=True, old_tree=None, new_tree=None):
621
path = inv.id2path(file_id)
622
if not path.startswith('/'):
624
return path, inv_entry.name, rev_tree.get_file_text(file_id)
626
def _parse_diffs(self, old_tree, new_tree, delta):
684
Return a nested data structure containing the changes in a delta::
686
added: list((filename, file_id)),
687
renamed: list((old_filename, new_filename, file_id)),
688
deleted: list((filename, file_id)),
628
Return a list of processed diffs, in the format::
702
if C{get_diffs} is false, the C{chunks} will be omitted.
646
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
648
process.append((old_path, new_path, fid, kind))
649
for path, fid, kind, text_modified, meta_modified in delta.modified:
650
process.append((path, path, fid, kind))
652
for old_path, new_path, fid, kind in process:
653
old_lines = old_tree.get_file_lines(fid)
654
new_lines = new_tree.get_file_lines(fid)
656
if old_lines != new_lines:
658
bzrlib.diff.internal_diff(old_path, old_lines,
659
new_path, new_lines, buffer)
660
except bzrlib.errors.BinaryFile:
663
diff = buffer.getvalue()
666
out.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=self._process_diff(diff), raw_diff=diff))
670
def _process_diff(self, diff):
671
# doesn't really need to be a method; could be static.
674
for line in diff.splitlines():
677
if line.startswith('+++ ') or line.startswith('--- '):
679
if line.startswith('@@ '):
681
if chunk is not None:
683
chunk = util.Container()
685
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
686
old_lineno = lines[0]
687
new_lineno = lines[1]
688
elif line.startswith(' '):
689
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
690
type='context', line=util.fixed_width(line[1:])))
693
elif line.startswith('+'):
694
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
695
type='insert', line=util.fixed_width(line[1:])))
697
elif line.startswith('-'):
698
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
699
type='delete', line=util.fixed_width(line[1:])))
702
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
703
type='unknown', line=util.fixed_width(repr(line))))
704
if chunk is not None:
708
def parse_delta(self, delta):
710
Return a nested data structure containing the changes in a delta::
712
added: list((filename, file_id)),
713
renamed: list((old_filename, new_filename, file_id)),
714
deleted: list((filename, file_id)),
709
def rich_filename(path, kind):
710
if kind == 'directory':
712
if kind == 'symlink':
716
def process_diff(diff):
719
for line in diff.splitlines():
722
if line.startswith('+++ ') or line.startswith('--- '):
724
if line.startswith('@@ '):
726
if chunk is not None:
728
chunk = util.Container()
730
lines = [int(x.split(',')[0][1:]) for x in line.split(' ')[1:3]]
731
old_lineno = lines[0]
732
new_lineno = lines[1]
733
elif line.startswith(' '):
734
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=new_lineno,
735
type='context', line=util.html_clean(line[1:])))
738
elif line.startswith('+'):
739
chunk.diff.append(util.Container(old_lineno=None, new_lineno=new_lineno,
740
type='insert', line=util.html_clean(line[1:])))
742
elif line.startswith('-'):
743
chunk.diff.append(util.Container(old_lineno=old_lineno, new_lineno=None,
744
type='delete', line=util.html_clean(line[1:])))
747
chunk.diff.append(util.Container(old_lineno=None, new_lineno=None,
748
type='unknown', line=util.html_clean(repr(line))))
749
if chunk is not None:
753
def handle_modify(old_path, new_path, fid, kind):
755
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
757
old_lines = old_tree.get_file_lines(fid)
758
new_lines = new_tree.get_file_lines(fid)
760
bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer)
761
diff = buffer.getvalue()
762
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid, chunks=process_diff(diff), raw_diff=diff))
764
725
for path, fid, kind in delta.added:
765
726
added.append((rich_filename(path, kind), fid))
767
728
for path, fid, kind, text_modified, meta_modified in delta.modified:
768
handle_modify(path, path, fid, kind)
770
for oldpath, newpath, fid, kind, text_modified, meta_modified in delta.renamed:
771
renamed.append((rich_filename(oldpath, kind), rich_filename(newpath, kind), fid))
729
modified.append(util.Container(filename=rich_filename(path, kind), file_id=fid))
731
for old_path, new_path, fid, kind, text_modified, meta_modified in delta.renamed:
732
renamed.append((rich_filename(old_path, kind), rich_filename(new_path, kind), fid))
772
733
if meta_modified or text_modified:
773
handle_modify(oldpath, newpath, fid, kind)
734
modified.append(util.Container(filename=rich_filename(new_path, kind), file_id=fid))
775
736
for path, fid, kind in delta.removed:
776
737
removed.append((rich_filename(path, kind), fid))
778
739
return util.Container(added=added, renamed=renamed, removed=removed, modified=modified)
783
744
for change in changes:
784
745
for m in change.changes.modified:
785
746
m.sbs_chunks = _make_side_by_side(m.chunks)
788
def get_filelist(self, inv, path, sort_type=None):
748
def get_filelist(self, inv, file_id, sort_type=None):
790
750
return the list of all files (and their attributes) within a given
793
while path.endswith('/'):
795
if path.startswith('/'):
798
entries = inv.entries()
801
for filepath, entry in entries:
802
fetch_set.add(entry.revision)
803
change_dict = dict([(c.revid, c) for c in self.get_changes(list(fetch_set))])
754
dir_ie = inv[file_id]
755
path = inv.id2path(file_id)
806
for filepath, entry in entries:
807
if posixpath.dirname(filepath) != path:
809
filename = posixpath.basename(filepath)
810
rich_filename = filename
760
for filename, entry in dir_ie.children.iteritems():
761
revid_set.add(entry.revision)
764
for change in self.get_changes(list(revid_set)):
765
change_dict[change.revid] = change
767
for filename, entry in dir_ie.children.iteritems():
811
768
pathname = filename
812
769
if entry.kind == 'directory':
816
772
revid = entry.revision
817
change = change_dict[revid]
819
file = util.Container(filename=filename, rich_filename=rich_filename, executable=entry.executable, kind=entry.kind,
820
pathname=pathname, file_id=entry.file_id, size=entry.text_size, revid=revid, change=change)
774
file = util.Container(
775
filename=filename, executable=entry.executable, kind=entry.kind,
776
pathname=pathname, file_id=entry.file_id, size=entry.text_size,
777
revid=revid, change=change_dict[revid])
821
778
file_list.append(file)
823
if sort_type == 'filename':
824
file_list.sort(key=lambda x: x.filename)
780
if sort_type == 'filename' or sort_type is None:
781
file_list.sort(key=lambda x: x.filename.lower()) # case-insensitive
825
782
elif sort_type == 'size':
826
783
file_list.sort(key=lambda x: x.size)
827
784
elif sort_type == 'date':
828
785
file_list.sort(key=lambda x: x.change.date)
787
# Always sort by kind to get directories first
788
file_list.sort(key=lambda x: x.kind != 'directory')
831
791
for file in file_list:
832
792
file.parity = parity