home » zplus/clif.git
Author zPlus <zplus@peers.community> 2023-02-26 22:49:35
Committer zPlus <zplus@peers.community> 2023-02-26 22:49:35
Commit 1343aaf (patch)
Tree b4891e6
Parent(s)

Replace blank lines with empty lines. This was actually done automatically by the text editor since I changed its configuration.


commits diff: 4cefe17..1343aaf
1 file changed, 159 insertions, 159 deletionsdownload


Diffstat
-rw-r--r-- web.py 318

Diff options
View
Side
Whitespace
Context lines
Inter-hunk lines
+159/-159 M   web.py
index b143408..bb701cd
old size: 31K - new size: 30K
@@ -71,9 +71,9 @@ def list_repositories():
71 71 """
72 72 Scan GITOLITE_REPOSITORIES_ROOT for Git repositories, and return a list of them.
73 73 """
74 -
74 +
75 75 repositories = []
76 -
76 +
77 77 # When topdown is True, the caller can modify the dirnames list in-place and
78 78 # walk() will only recurse into the subdirectories whose names remain in dirnames;
79 79 # this can be used to prune the search.
@@ -81,25 +81,25 @@ def list_repositories():
81 81 for path, dirs, files in os.walk(GITOLITE_REPOSITORIES_ROOT, topdown=True):
82 82 # Remove all files, we only want to recurse into directories
83 83 files.clear()
84 -
84 +
85 85 # This path is a git repo. Remove all sub-dirs because we don't need to
86 86 # recurse any further
87 87 if path.endswith('.git'):
88 88 dirs.clear()
89 -
89 +
90 90 repository = os.path.relpath(path, GITOLITE_REPOSITORIES_ROOT)
91 -
91 +
92 92 # DO NOT LIST gitolite-admin repository!
93 93 # This is the administration repository of this instance!
94 94 if repository.lower() == 'gitolite-admin.git':
95 95 continue
96 -
96 +
97 97 try:
98 98 with open(os.path.join(path, 'description')) as f:
99 99 description = f.read()
100 100 except:
101 101 description = ''
102 -
102 +
103 103 repositories.append({
104 104 'path': repository,
105 105 'description': description
@@ -112,15 +112,15 @@ def parse_thread_tags(data):
112 112 """
113 113 Parse "tags" file of a mailing list thread.
114 114 """
115 -
115 +
116 116 tags = {}
117 -
117 +
118 118 for line in data.splitlines():
119 119 k, v = line.split('=', 1)
120 120 k = k.strip()
121 121 v = v.strip()
122 122 tags[k] = tags.get(k, []) + [ v ]
123 -
123 +
124 124 return tags
125 125
126 126
@@ -146,11 +146,11 @@ def human_size(bytes, B=False):
146 146 Convert a file size in bytes to a human friendly form.
147 147 This is only used in templates when showing file sizes.
148 148 """
149 -
149 +
150 150 for unit in [ 'B' if B else '', 'K', 'M', 'G', 'T', 'P' ]:
151 151 if bytes < 1024: break
152 152 bytes = bytes / 1024
153 -
153 +
154 154 return '{}{}'.format(round(bytes), unit).rjust(5)
155 155
156 156 def humanct(commit_time, commit_time_offset = 0):
@@ -158,12 +158,12 @@ def humanct(commit_time, commit_time_offset = 0):
158 158 The following will add custom functions to the jinja2 template engine.
159 159 These will be available to use within templates.
160 160 """
161 -
161 +
162 162 delta = datetime.timedelta(minutes=commit_time_offset)
163 163 tz = datetime.timezone(delta)
164 164
165 165 dt = datetime.datetime.fromtimestamp(commit_time, tz)
166 -
166 +
167 167 return dt.astimezone(pytz.utc).strftime('%Y-%m-%d %H:%M:%S')
168 168
169 169 template = functools.partial(template, template_settings = {
@@ -197,10 +197,10 @@ template = functools.partial(template, template_settings = {
197 197 def error404(error):
198 198 """
199 199 Custom 404 page.
200 -
200 +
201 201 :param error: bottle.HTTPError given by Bottle when calling abort(404).
202 202 """
203 -
203 +
204 204 return '[404] {}'.format(error.body)
205 205
206 206 @bottle.get('/static/<filename:path>', name='static')
@@ -216,7 +216,7 @@ def explore():
216 216 """
217 217 The home page displayed at https://domain/
218 218 """
219 -
219 +
220 220 repositories = list_repositories()
221 221 return template('explore.html', repositories=repositories)
222 222
@@ -225,29 +225,29 @@ def about():
225 225 """
226 226 The home page displayed at https://domain/
227 227 """
228 -
228 +
229 229 return template('about.html', domain=INSTANCE_DOMAIN)
230 230
231 231 @bottle.get('/<repository:path>.git', name='overview')
232 232 def overview(repository):
233 233 """
234 234 Show README and other info about the repository.
235 -
235 +
236 236 :param repository: Match repository name ending with ".git"
237 237 """
238 -
238 +
239 239 repository += '.git'
240 240 path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
241 241
242 242 if not os.path.isdir(path):
243 243 bottle.abort(404, 'No repository at this path.')
244 -
244 +
245 245 repo = pygit2.Repository(path)
246 246 local_branches = list(repo.branches.local)
247 -
247 +
248 248 HEAD = None
249 249 ref_name = None
250 -
250 +
251 251 try:
252 252 HEAD = repo.head.name
253 253 ref_name = HEAD
@@ -256,31 +256,31 @@ def overview(repository):
256 256 if name_candidate in local_branches:
257 257 ref_name = name_candidate
258 258 break
259 -
259 +
260 260 readme = ''
261 -
261 +
262 262 if ref_name:
263 263 tree = repo.revparse_single(ref_name).tree
264 -
264 +
265 265 for e in tree:
266 266 if e.name.lower() not in [ 'readme', 'readme.md', 'readme.rst' ]:
267 267 continue
268 -
268 +
269 269 if e.is_binary:
270 270 continue
271 -
271 +
272 272 # Read the README content, cut at 1MB
273 273 readme = tree[e.name].data[:1048576].decode('UTF-8')
274 274 break
275 -
275 +
276 276 repo_size = sum(f.stat().st_size for f in pathlib.Path(path).glob("**/*"))
277 -
277 +
278 278 try:
279 279 with open(os.path.join(path, 'description')) as f:
280 280 description = f.read()
281 281 except:
282 282 description = ''
283 -
283 +
284 284 return template('repository/overview.html',
285 285 readme=readme,
286 286 repository=repository,
@@ -293,60 +293,60 @@ def refs(repository):
293 293 """
294 294 List repository refs
295 295 """
296 -
296 +
297 297 repository += '.git'
298 298 path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
299 -
299 +
300 300 if not os.path.isdir(path):
301 301 bottle.abort(404, 'No repository at this path.')
302 -
302 +
303 303 repo = pygit2.Repository(path)
304 -
304 +
305 305 if repo.is_empty:
306 306 return template('repository/refs.html',
307 307 repository=repository)
308 -
308 +
309 309 try: HEAD = repo.head.name
310 310 except: HEAD = None
311 -
311 +
312 312 heads = []
313 313 tags = []
314 -
314 +
315 315 for ref in repo.references:
316 316 ref = repo.references.get(ref)
317 -
317 +
318 318 if not ref:
319 319 continue
320 -
320 +
321 321 if ref.name.startswith('refs/heads/'):
322 322 heads.append({
323 323 'ref': ref,
324 324 'commit': ref.peel(pygit2.GIT_OBJ_COMMIT)
325 325 })
326 -
326 +
327 327 if ref.name.startswith('refs/tags/'):
328 328 target = repo.get(str(ref.target))
329 -
329 +
330 330 tags.append({
331 331 'ref': ref,
332 332 'object': target,
333 333 'is_annotated': target.type == pygit2.GIT_OBJ_TAG
334 334 })
335 -
335 +
336 336 heads.sort(key = lambda item: item['ref'].name)
337 -
337 +
338 338 def tagsort(item):
339 339 try:
340 340 if item['object'].type == pygit2.GIT_OBJ_TAG:
341 341 return item['object'].tagger.time
342 -
342 +
343 343 if item['object'].type == pygit2.GIT_OBJ_COMMIT:
344 344 return item['object'].commit_time
345 345 except:
346 346 return 0
347 -
347 +
348 348 tags.sort(key = lambda item: tagsort(item), reverse=True)
349 -
349 +
350 350 return template('repository/refs.html',
351 351 repository=repository,
352 352 heads=heads, tags=tags, HEAD=HEAD)
@@ -357,24 +357,24 @@ def tree(repository, revision, tree_path=None):
357 357 """
358 358 Show commit tree.
359 359 """
360 -
360 +
361 361 repository += '.git'
362 362 repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
363 -
363 +
364 364 if not os.path.isdir(repository_path):
365 365 bottle.abort(404, 'No repository at this path.')
366 -
366 +
367 367 repo = pygit2.Repository(repository_path)
368 -
368 +
369 369 if repo.is_empty:
370 370 return template('repository/tree.html',
371 371 repository=repository, revision=revision, offset=0)
372 -
372 +
373 373 try:
374 374 git_object = repo.revparse_single(revision)
375 375 except:
376 376 bottle.abort(404)
377 -
377 +
378 378 # List all the references.
379 379 # This is used for allowing the user to switch revision with a selector.
380 380 HEAD = None
@@ -385,21 +385,21 @@ def tree(repository, revision, tree_path=None):
385 385 if ref.startswith('refs/tags/'): tags.append(ref)
386 386 heads.sort()
387 387 tags.sort()
388 -
388 +
389 389 try:
390 390 HEAD = repo.head.name
391 391 except:
392 392 pass
393 -
393 +
394 394 if git_object.type == pygit2.GIT_OBJ_TAG:
395 395 git_object = git_object.peel(None)
396 -
396 +
397 397 if git_object.type == pygit2.GIT_OBJ_COMMIT:
398 398 git_object = git_object.tree
399 -
399 +
400 400 if git_object.type == pygit2.GIT_OBJ_TREE and tree_path:
401 401 git_object = git_object[tree_path]
402 -
402 +
403 403 if git_object.type == pygit2.GIT_OBJ_TREE:
404 404 return template(
405 405 'repository/tree.html',
@@ -407,26 +407,26 @@ def tree(repository, revision, tree_path=None):
407 407 tree=git_object,
408 408 tree_path=tree_path,
409 409 repository=repository, revision=revision)
410 -
410 +
411 411 if git_object.type == pygit2.GIT_OBJ_BLOB:
412 -
412 +
413 413 # Highlight blob text
414 414 if git_object.is_binary:
415 415 blob_formatted = ''
416 416 else:
417 417 blob_data = git_object.data.decode('UTF-8')
418 -
418 +
419 419 # Guess Pygments lexer by filename, or by content if we can't find one
420 420 try:
421 421 pygments_lexer = guess_lexer_for_filename(git_object.name, blob_data)
422 422 except:
423 423 pygments_lexer = guess_lexer(blob_data)
424 -
424 +
425 425 pygments_formatter = HtmlFormatter(nobackground=True, linenos=True, anchorlinenos=True,
426 426 lineanchors='line')
427 -
427 +
428 428 blob_formatted = highlight(blob_data, pygments_lexer, pygments_formatter)
429 -
429 +
430 430 return template(
431 431 'repository/blob.html',
432 432 heads=heads, tags=tags,
@@ -434,7 +434,7 @@ def tree(repository, revision, tree_path=None):
434 434 blob_formatted=blob_formatted,
435 435 repository=repository, revision=revision,
436 436 tree_path=tree_path)
437 -
437 +
438 438 bottle.abort(404)
439 439
440 440 @bottle.post('/<repository:path>.git/tree', name='tree_change')
@@ -444,9 +444,9 @@ def tree_change(repository):
444 444 This route is used by the <form> in the tree page when changing the revision
445 445 to be displayed.
446 446 """
447 -
447 +
448 448 revision = request.forms.get('revision')
449 -
449 +
450 450 bottle.redirect(application.get_url('tree',
451 451 repository=repository,
452 452 revision=revision))
@@ -456,28 +456,28 @@ def log(repository, revision):
456 456 """
457 457 Show commit log.
458 458 """
459 -
459 +
460 460 repository += '.git'
461 461 repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
462 -
462 +
463 463 # Read commits
464 464 try: commits_offset = int(request.query.get('offset', 0))
465 465 except: commits_offset = 0
466 -
466 +
467 467 if not os.path.isdir(repository_path):
468 468 bottle.abort(404, 'No repository at this path.')
469 -
469 +
470 470 repo = pygit2.Repository(repository_path)
471 -
471 +
472 472 if repo.is_empty:
473 473 return template('repository/log.html',
474 474 repository=repository, revision=revision, offset=commits_offset)
475 -
475 +
476 476 try:
477 477 git_object = repo.revparse_single(revision)
478 478 except:
479 479 bottle.abort(404)
480 -
480 +
481 481 # List all the references.
482 482 # This is used for allowing the user to switch revision with a selector.
483 483 HEAD = None
@@ -488,20 +488,20 @@ def log(repository, revision):
488 488 if ref.startswith('refs/tags/'): tags.append(ref)
489 489 heads.sort()
490 490 tags.sort()
491 -
491 +
492 492 try:
493 493 HEAD = repo.head.name
494 494 except:
495 495 pass
496 -
496 +
497 497 if git_object.type in [ pygit2.GIT_OBJ_TREE, pygit2.GIT_OBJ_BLOB ]:
498 498 return 'Not a valid ref'
499 -
499 +
500 500 if git_object.type == pygit2.GIT_OBJ_TAG:
501 501 git_object = git_object.peel(None)
502 -
502 +
503 503 # At this point git_object should be a valid pygit2.GIT_OBJ_COMMIT
504 -
504 +
505 505 commits = []
506 506 diff = {}
507 507 commit_ith = 0
@@ -510,20 +510,20 @@ def log(repository, revision):
510 510 if commit_ith < commits_offset:
511 511 commit_ith += 1
512 512 continue
513 -
513 +
514 514 # Stop if we have reached pagination size
515 515 if len(commits) >= LOG_PAGINATION:
516 516 break
517 -
517 +
518 518 commits.append(commit)
519 -
519 +
520 520 # Diff with parent tree, or empty tree if there's no parent
521 521 if LOG_STATS:
522 522 diff[commit.short_id] = \
523 523 commit.parents[0].tree.diff_to_tree(commit.tree) \
524 524 if len(commit.parents) > 0 \
525 525 else commit.tree.diff_to_tree(swap=True)
526 -
526 +
527 527 return template(
528 528 'repository/log.html',
529 529 heads=heads, head_ref=HEAD, tags=tags,
@@ -538,9 +538,9 @@ def log_change(repository):
538 538 This route is used by the <form> in the log page when changing the revision
539 539 to be displayed.
540 540 """
541 -
541 +
542 542 revision = request.forms.get('revision')
543 -
543 +
544 544 bottle.redirect(application.get_url('log',
545 545 repository=repository,
546 546 revision=revision))
@@ -554,19 +554,19 @@ def commit(repository, commit_id, commit_id2=None):
554 554 """
555 555 Show a commit.
556 556 """
557 -
557 +
558 558 repository += '.git'
559 559 repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
560 -
560 +
561 561 if not os.path.isdir(repository_path):
562 562 bottle.abort(404, 'No repository at this path.')
563 -
563 +
564 564 repo = pygit2.Repository(repository_path)
565 -
565 +
566 566 try:
567 567 commit = repo.get(commit_id)
568 568 assert commit.type == pygit2.GIT_OBJ_COMMIT
569 -
569 +
570 570 if commit_id2:
571 571 commit2 = repo.get(commit_id2)
572 572 assert commit2.type == pygit2.GIT_OBJ_COMMIT
@@ -577,22 +577,22 @@ def commit(repository, commit_id, commit_id2=None):
577 577 commit2 = None
578 578 except:
579 579 bottle.abort(404, 'Not a valid commit.')
580 -
580 +
581 581 # Diff options
582 -
582 +
583 583 diff_mode = DIFF_VIEW
584 584 if 'mode' in request.query:
585 585 if request.query.get('mode') in [ 'udiff', 'udiff_raw', 'ssdiff' ]:
586 586 diff_mode = request.query.get('mode')
587 587 else:
588 588 bottle.abort(400, 'Bad request: mode')
589 -
589 +
590 590 try: diff_context_lines = int(request.query.get('context_lines', DIFF_CONTEXT_LINES))
591 591 except: bottle.abort(400, 'Bad request: context_lines')
592 -
592 +
593 593 try: diff_inter_hunk_lines = int(request.query.get('inter_hunk_lines', DIFF_INTERHUNK_LINES))
594 594 except: bottle.abort(400, 'Bad request: inter_hunk_lines')
595 -
595 +
596 596 diff_flags = pygit2.GIT_DIFF_NORMAL
597 597 diff_side = DIFF_SIDE
598 598 if 'side' in request.query:
@@ -603,7 +603,7 @@ def commit(repository, commit_id, commit_id2=None):
603 603 diff_side = 'reverse'
604 604 else:
605 605 bottle.abort(400, 'Bad request: side')
606 -
606 +
607 607 diff_whitespace = DIFF_WHITESPACE
608 608 if 'whitespace' in request.query:
609 609 if request.query.get('whitespace') == 'include':
@@ -619,9 +619,9 @@ def commit(repository, commit_id, commit_id2=None):
619 619 diff_whitespace = 'ignore_eol'
620 620 else:
621 621 bottle.abort(400, 'Bad request: whitespace')
622 -
622 +
623 623 # Compute diff with parent
624 -
624 +
625 625 if commit2:
626 626 diff = repo.diff(
627 627 a=commit2, b=commit, flags=diff_flags,
@@ -633,21 +633,21 @@ def commit(repository, commit_id, commit_id2=None):
633 633 interhunk_lines = diff_inter_hunk_lines,
634 634 flags = diff_flags,
635 635 swap = True)
636 -
636 +
637 637 # Compute the similarity index. This is used to decide which files are "renamed".
638 638 diff.find_similar()
639 -
639 +
640 640 if request.route.name in [ 'commit_diff', 'commit_diff2' ]:
641 641 response.content_type = 'text/plain'
642 642 return diff.patch
643 -
643 +
644 644 if request.route.name == 'commit_patch':
645 645 """
646 646 Looks like pygit2 doesn't have a way for creating a patch file from a commit.
647 647 So, exclusively for this task, we fork a new subprocess and read the output.
648 648 TODO see if this subprocess call can be replaced with pygit2.
649 649 """
650 -
650 +
651 651 try:
652 652 output = subprocess.check_output(
653 653 args=[ 'git', 'format-patch',
@@ -657,13 +657,13 @@ def commit(repository, commit_id, commit_id2=None):
657 657 '--inter-hunk-context={}'.format(diff_inter_hunk_lines),
658 658 '-1', str(commit.id) ],
659 659 cwd=repository_path)
660 -
660 +
661 661 response.content_type = 'text/plain'
662 662 return output
663 663 except Exception as e:
664 664 print(e)
665 665 bottle.abort(500, 'Cannot create patch.')
666 -
666 +
667 667 return template(
668 668 'repository/commit.html',
669 669 repository=repository, commit=commit, commit2=commit2, diff=diff,
@@ -675,38 +675,38 @@ def raw(repository, revision, tree_path):
675 675 """
676 676 Return a raw blow object.
677 677 """
678 -
678 +
679 679 repository += '.git'
680 680 repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
681 -
681 +
682 682 if not os.path.isdir(repository_path):
683 683 bottle.abort(404, 'No repository at this path.')
684 -
684 +
685 685 repo = pygit2.Repository(repository_path)
686 -
686 +
687 687 if repo.is_empty:
688 688 return ""
689 -
689 +
690 690 git_tree = None
691 -
691 +
692 692 try:
693 693 git_object = repo.revparse_single(revision)
694 694 except:
695 695 pass
696 -
696 +
697 697 if not git_object or git_object.type != pygit2.GIT_OBJ_COMMIT:
698 698 bottle.abort(404, 'Not a valid revision.')
699 -
699 +
700 700 blob = None
701 -
701 +
702 702 try:
703 703 blob = git_object.tree[tree_path]
704 704 except:
705 705 bottle.abort(404, 'Object does not exist.')
706 -
706 +
707 707 if blob.type != pygit2.GIT_OBJ_BLOB:
708 708 bottle.abort(404, 'Object is not a blob.')
709 -
709 +
710 710 mime = magic.from_buffer(blob.data[:1048576], mime=True)
711 711 response.content_type = mime
712 712 return blob.data
@@ -721,25 +721,25 @@ def git_smart_http(repository):
721 721 Note that this controller only matches "git-upload-pack" (used for fetching)
722 722 but does not match "git-receive-pack" (used for pushing). Pushing should only
723 723 happen via SSH.
724 -
724 +
725 725 Note: If CLIF is running behind a web server such as httpd or lighttpd, the
726 726 same behavior of this controller can be achieved much more simply by configuring
727 727 the server with CGI and an alias that redirects the URLs above to the gitolite-shell
728 728 script. However, this controller exists so that anonymous HTTP clones can work
729 729 "out of the box" without any manual configuration of the server.
730 -
730 +
731 731 Documentation useful for understanding how this works:
732 732 https://git-scm.com/docs/http-protocol
733 733 https://bottlepy.org/docs/dev/async.html
734 734 https://gitolite.com/gitolite/http.html#allowing-unauthenticated-access
735 735 """
736 -
736 +
737 737 # Environment variables for the Gitolite shell
738 738 # TODO Gitolite gives a warning: "WARNING: Use of uninitialized value in concatenation (.) or string at /home/git/bin/gitolite-shell line 239"
739 739 # Looks like some non-critical env vars are missing here: REMOTE_PORT SERVER_ADDR SERVER_PORT
740 740 gitenv = {
741 741 **os.environ,
742 -
742 +
743 743 # https://git-scm.com/docs/git-http-backend#_environment
744 744 'PATH_INFO': request.path,
745 745 'REMOTE_USER': 'anonymous', # This user must be set in ~/.gitolite.rc like this:
@@ -750,13 +750,13 @@ def git_smart_http(repository):
750 750 'REQUEST_METHOD': request.method,
751 751 'GIT_PROJECT_ROOT': GITOLITE_REPOSITORIES_ROOT,
752 752 'GIT_HTTP_EXPORT_ALL': 'true',
753 -
753 +
754 754 # Additional variables required by Gitolite
755 755 'REQUEST_URI': request.fullpath,
756 756 'GITOLITE_HTTP_HOME': GITOLITE_HTTP_HOME,
757 757 'HOME': GITOLITE_HTTP_HOME,
758 758 }
759 -
759 +
760 760 # Start a Gitolite shell.
761 761 # Do not replace .Popen() with .run() because it waits for child process to finish before returning.
762 762 proc = subprocess.Popen(
@@ -765,14 +765,14 @@ def git_smart_http(repository):
765 765 stdin = subprocess.PIPE,
766 766 stdout = subprocess.PIPE)
767 767 # stderr = )
768 -
768 +
769 769 # Write the whole request body to Gitolite stdin.
770 770 # Don't forget to close the pipe or it will hang!
771 771 proc.stdin.write(request.body.read())
772 772 proc.stdin.close()
773 -
773 +
774 774 # Now we process the Gitolite response and return it to the client.
775 -
775 +
776 776 # First we need to scan all the HTTP headers in the response so that we can
777 777 # add them to the bottle response...
778 778 for line in proc.stdout:
@@ -793,63 +793,63 @@ def git_smart_http(repository):
793 793 def threads(repository):
794 794 """
795 795 List email threads.
796 -
796 +
797 797 :param repository: Match repository name NOT ending with ".git"
798 798 """
799 -
799 +
800 800 # List of seletected tags, retrieved from the query string
801 801 query_tags = { k: request.query.getall(k) for k in request.query.keys() }
802 -
802 +
803 803 repository += '.mlist.git'
804 804 path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
805 805 list_address = '{}@{}'.format(repository[:-10], INSTANCE_DOMAIN)
806 -
806 +
807 807 if not os.path.isdir(path):
808 808 bottle.abort(404, 'No repository at this path.')
809 -
809 +
810 810 try:
811 811 repo = pygit2.Repository(path)
812 812 tree = repo.revparse_single('HEAD').tree
813 813 except:
814 814 return template('mailing_list/emails.html', list_address=list_address, repository=repository)
815 -
815 +
816 816 threads_list = []
817 817 tags = {}
818 -
818 +
819 819 for obj in tree:
820 820 if obj.type != pygit2.GIT_OBJ_TREE:
821 821 continue
822 -
822 +
823 823 thread_date, thread_time, thread_id, thread_title = obj.name.split(' ', 3)
824 -
824 +
825 825 try:
826 826 thread_tags = parse_thread_tags(obj['tags'].data.decode('UTF-8'))
827 -
827 +
828 828 # Collect tags for filters
829 829 for k, v in thread_tags.items():
830 830 tags[k] = tags.get(k, set()).union(v)
831 831 except:
832 832 thread_tags = {}
833 -
833 +
834 834 # Check if we should filter out this thread from the list
835 835 keep = True
836 836 for key in query_tags.keys():
837 837 for value in query_tags[key]:
838 838 action, value = value[0], value[1:]
839 -
839 +
840 840 if action not in [ '+', '-' ]:
841 841 bottle.abort(400, 'Bad request: {}'.format(key))
842 -
842 +
843 843 if action == '+' and value not in thread_tags.get(key, []):
844 844 keep = False
845 845 break
846 -
846 +
847 847 if action == '-' and value in thread_tags.get(key, []):
848 848 keep = False
849 849 break
850 -
850 +
851 851 if not keep: break
852 -
852 +
853 853 if keep:
854 854 threads_list.append({
855 855 'datetime': thread_date + ' ' + thread_time,
@@ -857,9 +857,9 @@ def threads(repository):
857 857 'title': thread_title,
858 858 'tags': thread_tags
859 859 })
860 -
860 +
861 861 threads_list.reverse()
862 -
862 +
863 863 return template('mailing_list/emails.html', threads=threads_list,
864 864 list_address=list_address,
865 865 repository=repository,
@@ -870,55 +870,55 @@ def thread(repository, thread_id):
870 870 """
871 871 Show a single email thread.
872 872 """
873 -
873 +
874 874 repository += '.mlist.git'
875 875 path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository)
876 876 list_address = '{}@{}'.format(repository[:-10], INSTANCE_DOMAIN)
877 -
877 +
878 878 if not os.path.isdir(path):
879 879 bottle.abort(404, 'No repository at this path.')
880 -
880 +
881 881 repo = pygit2.Repository(path)
882 882 head_tree = repo.revparse_single('HEAD').tree
883 883 thread_tree = None
884 -
884 +
885 885 for obj in head_tree:
886 886 if obj.type != pygit2.GIT_OBJ_TREE:
887 887 continue
888 -
888 +
889 889 if thread_id in obj.name:
890 890 thread_tree = obj
891 891 break
892 -
892 +
893 893 if not thread_tree:
894 894 bottle.abort(404, 'Not a valid thread')
895 -
895 +
896 896 thread_date, thread_time, thread_id, thread_title = thread_tree.name.split(' ', 3)
897 897 thread_data = {
898 898 'datetime': thread_date + ' ' + thread_time,
899 899 'id': thread_id,
900 900 'title': thread_title
901 901 }
902 -
902 +
903 903 # Read all the emails in this thread and collect some statistics on the way (for
904 904 # displaying purposes only)
905 905 emails = []
906 906 participants = []
907 907 tags = {}
908 -
908 +
909 909 for obj in thread_tree:
910 910 if obj.type != pygit2.GIT_OBJ_BLOB:
911 911 continue
912 -
912 +
913 913 if obj.name == 'tags':
914 914 tags = parse_thread_tags(obj.data.decode('UTF-8'))
915 915 continue
916 -
916 +
917 917 if not obj.name.endswith('.email'):
918 918 continue
919 -
919 +
920 920 message = email.message_from_string(obj.data.decode('UTF-8'), policy=email.policy.default)
921 -
921 +
922 922 email_data = {
923 923 'id': message.get('message-id'),
924 924 'id_hash': hashlib.sha256(message.get('message-id').encode('utf-8')).hexdigest()[:8],
@@ -930,14 +930,14 @@ def thread(repository, thread_id):
930 930 'subject': message.get('subject'),
931 931 'body': message.get_body(('plain',)).get_content()
932 932 }
933 -
933 +
934 934 emails.append(email_data)
935 -
935 +
936 936 if email_data['from'] not in participants:
937 937 participants.append(email_data['from'])
938 -
938 +
939 939 emails.sort(key = lambda email: email['received_at'])
940 -
940 +
941 941 return template('mailing_list/emails_thread.html', thread=thread_data, emails=emails,
942 942 participants=participants, list_address=list_address, tags=tags,
943 943 repository=repository)