11#!/usr/bin/env python3
22
3+
34# This file is part of Elixir, a source code cross-referencer.
45#
56# Copyright (C) 2017--2020 Mikaël Bouillot <mikael.bouillot@bootlin.com>
2829import dataclasses
2930from collections import OrderedDict , namedtuple
3031from re import search , sub
31- from typing import Any , Callable , NamedTuple , Tuple
32+ from typing import Any , Callable , NamedTuple , Optional , Tuple
33+ from difflib import SequenceMatcher
3234from urllib import parse
3335import falcon
3436import jinja2
4244from .query import get_query
4345from .web_utils import ProjectConverter , IdentConverter , validate_version , validate_project , validate_ident , \
4446 get_elixir_version_string , get_elixir_repo_url , RequestContext , Config , DirectoryEntry
45- from .diff import format_diff
47+ from .diff import format_diff , diff_directory_entries
4648
4749VERSION_CACHE_DURATION_SECONDS = 2 * 60 # 2 minutes
4850ADD_ISSUE_LINK = "https://github.com/bootlin/elixir/issues/new"
@@ -110,7 +112,7 @@ def get_project_error_page(req, resp, exception: ElixirProjectError):
110112
111113 versions_raw = get_versions_cached (query , req .context , project )
112114 get_url_with_new_version = lambda v : stringify_source_path (project , v , '/' )
113- versions , current_version_path = get_versions (versions_raw , get_url_with_new_version , version )
115+ versions , current_version_path = get_versions (versions_raw , get_url_with_new_version , None , version )
114116
115117 if current_version_path [2 ] is None :
116118 # If details about current version are not available, make base links
@@ -194,6 +196,10 @@ def validate_project_and_version(ctx, project, version):
194196def get_source_base_url (project : str , version : str ) -> str :
195197 return f'/{ parse .quote (project , safe = "" ) } /{ parse .quote (version , safe = "" ) } /source'
196198
199+ def stringify_diff_path (project : str , version : str , version_other : str , path : str ) -> str :
200+ return f'/{ parse .quote (project , safe = "" ) } /{ parse .quote (version , safe = "" ) } /diff/' + \
201+ f'{ parse .quote (version_other , safe = "" ) } /{ path } '
202+
197203# Converts ParsedSourcePath to a string with corresponding URL path
198204def stringify_source_path (project : str , version : str , path : str ) -> str :
199205 if not path .startswith ('/' ):
@@ -251,14 +257,19 @@ def on_get(self, req, resp, project: str, version: str, path: str):
251257
252258 query .close ()
253259
260+ # Returns base url of diff pages
261+ # project and version are assumed to be unquoted
262+ def get_diff_base_url (project : str , version : str , version_other : str ) -> str :
263+ return f'/{ parse .quote (project , safe = "" ) } /{ parse .quote (version , safe = "" ) } /diff/{ parse .quote (version_other , safe = "" ) } '
264+
254265# Handles source URLs without a path, ex. '/u-boot/v2023.10/source'.
255266# Note lack of trailing slash
256267class SourceWithoutPathResource (SourceResource ):
257268 def on_get (self , req , resp , project : str , version : str ):
258269 return super ().on_get (req , resp , project , version , '' )
259270
260271class DiffResource :
261- def on_get (self , req , resp , project : str , version : str , version_other : str , path : str ):
272+ def on_get (self , req , resp , project : str , version : str , version_other : str , path : str = '' ):
262273 project , version , query = validate_project_and_version (req .context , project , version )
263274 version_other = validate_version (parse .unquote (version_other ))
264275 if version_other is None or version_other == 'latest' :
@@ -432,7 +443,7 @@ def get_projects(basedir: str) -> list[ProjectEntry]:
432443
433444# Tuple of version name and URL to chosen resource with that version
434445# Used to render version list in the sidebar
435- VersionEntry = namedtuple ('VersionEntry' , 'version, url' )
446+ VersionEntry = namedtuple ('VersionEntry' , 'version, url, diff_url ' )
436447
437448# Takes result of Query.get_versions() and prepares it for the sidebar template.
438449# Returns an OrderedDict with version information and optionally a triple with
@@ -445,6 +456,7 @@ def get_projects(basedir: str) -> list[ProjectEntry]:
445456# current_version: string with currently browsed version
446457def get_versions (versions : OrderedDict [str , OrderedDict [str , str ]],
447458 get_url : Callable [[str ], str ],
459+ get_diff_url : Optional [Callable [[str ], str ]],
448460 current_version : str ) -> Tuple [dict [str , dict [str , list [VersionEntry ]]], Tuple [str | None , str | None , str | None ]]:
449461
450462 result = OrderedDict ()
@@ -456,13 +468,14 @@ def get_versions(versions: OrderedDict[str, OrderedDict[str, str]],
456468 result [major ] = OrderedDict ()
457469 if minor not in result [major ]:
458470 result [major ][minor ] = []
459- result [major ][minor ].append (VersionEntry (v , get_url (v )))
471+ result [major ][minor ].append (
472+ VersionEntry (v , get_url (v ), get_diff_url (v ) if get_diff_url is not None else None )
473+ )
460474 if v == current_version :
461475 current_version_path = (major , minor , v )
462476
463477 return result , current_version_path
464478
465- # Caches get_versions result in a context object
466479def get_versions_cached (q , ctx , project ):
467480 with ctx .versions_cache_lock :
468481 if project not in ctx .versions_cache :
@@ -481,9 +494,9 @@ def get_versions_cached(q, ctx, project):
481494# project: name of the project
482495# version: version of the project
483496def get_layout_template_context (q : Query , ctx : RequestContext , get_url_with_new_version : Callable [[str ], str ],
484- project : str , version : str ) -> dict [str , Any ]:
497+ get_diff_url : Optional [ Callable [[ str ], str ]], project : str , version : str ) -> dict [str , Any ]:
485498 versions_raw = get_versions_cached (q , ctx , project )
486- versions , current_version_path = get_versions (versions_raw , get_url_with_new_version , version )
499+ versions , current_version_path = get_versions (versions_raw , get_url_with_new_version , get_diff_url , version )
487500
488501 return {
489502 'projects' : get_projects (ctx .config .project_dir ),
@@ -658,21 +671,21 @@ def get_directory_entries(q: Query, base_url, tag: str, path: str) -> list[Direc
658671 lines = q .get_dir_contents (tag , path )
659672
660673 for l in lines :
661- type , name , size , perm = l .split (' ' )
674+ type , name , size , perm , blob_id = l .split (' ' )
662675 file_path = f"{ path } /{ name } "
663676
664677 if type == 'tree' :
665- dir_entries .append (DirectoryEntry ('tree' , name , file_path , f"{ base_url } { file_path } " , None ))
678+ dir_entries .append (DirectoryEntry ('tree' , name , file_path , f"{ base_url } { file_path } " , None , None ))
666679 elif type == 'blob' :
667680 # 120000 permission means it's a symlink
668681 if perm == '120000' :
669682 dir_path = path if path .endswith ('/' ) else path + '/'
670683 link_contents = q .get_file_raw (tag , file_path )
671684 link_target_path = os .path .abspath (dir_path + link_contents )
672685
673- dir_entries .append (DirectoryEntry ('symlink' , name , link_target_path , f"{ base_url } { link_target_path } " , size ))
686+ dir_entries .append (DirectoryEntry ('symlink' , name , link_target_path , f"{ base_url } { link_target_path } " , size , None ))
674687 else :
675- dir_entries .append (DirectoryEntry ('blob' , name , file_path , f"{ base_url } { file_path } " , size ))
688+ dir_entries .append (DirectoryEntry ('blob' , name , file_path , f"{ base_url } { file_path } " , size , None ))
676689
677690 return dir_entries
678691
@@ -727,10 +740,11 @@ def generate_source_page(ctx: RequestContext, q: Query,
727740 title_path = f'{ path_split [- 1 ] } - { "/" .join (path_split ) } - '
728741
729742 get_url_with_new_version = lambda v : stringify_source_path (project , v , path )
743+ get_diff_url = lambda v_other : stringify_diff_path (project , version , v_other , path )
730744
731745 # Create template context
732746 data = {
733- ** get_layout_template_context (q , ctx , get_url_with_new_version , project , version ),
747+ ** get_layout_template_context (q , ctx , get_url_with_new_version , get_diff_url , project , version ),
734748
735749 'title_path' : title_path ,
736750 'path' : path ,
@@ -747,15 +761,15 @@ def generate_diff_page(ctx: RequestContext, q: Query,
747761 project : str , version : str , version_other : str , path : str ) -> tuple [int , str ]:
748762
749763 status = falcon .HTTP_OK
750- source_base_url = get_source_base_url (project , version )
764+ diff_base_url = get_diff_base_url (project , version , version_other )
751765
752766 # Generate breadcrumbs
753767 path_split = path .split ('/' )[1 :]
754768 path_temp = ''
755- breadcrumb_links = []
769+ breadcrumb_urls = []
756770 for p in path_split :
757771 path_temp += '/' + p
758- breadcrumb_links .append ((p , f'{ source_base_url } { path_temp } ' ))
772+ breadcrumb_urls .append ((p , f'{ diff_base_url } { path_temp } ' ))
759773
760774 type = q .get_file_type (version , path )
761775 type_other = q .get_file_type (version_other , path )
@@ -795,25 +809,18 @@ def generate_warning(type, version):
795809 warning = f'Files are the same in { version } and { version_other } .'
796810 else :
797811 missing_version = version_other if type == 'blob' else version
798- warning = f'File does not exist or is not a file { missing_version } .'
812+ warning = f'File does not exist, or is not a file in { missing_version } . ( { version } displayed) '
799813
800814 template_ctx = {
801815 'code' : generate_source (q , project , version if type == 'blob' else version_other , path ),
802- 'warning' : warning
816+ 'warning' : warning ,
803817 }
804818 template = ctx .jinja_env .get_template ('source.html' )
805819 else :
806820 raise ElixirProjectError ('File not found' , f'This file does not exist in { version } nor in { version_other } .' ,
807821 status = falcon .HTTP_NOT_FOUND ,
808822 query = q , project = project , version = version ,
809- extra_template_args = {'breadcrumb_links' : breadcrumb_links })
810-
811- if type_other != 'blob' :
812- raise ElixirProjectError ('File not found' , f'This file is not present in { version_other } .' ,
813- status = falcon .HTTP_NOT_FOUND ,
814- query = q , project = project , version = version ,
815- extra_template_args = {'breadcrumb_links' : breadcrumb_links })
816-
823+ extra_template_args = {'breadcrumb_urls' : breadcrumb_urls })
817824
818825 # Create titles like this:
819826 # root path: "Linux source code (v5.5.6) - Bootlin"
@@ -827,20 +834,21 @@ def generate_warning(type, version):
827834 title_path = f'{ path_split [- 1 ] } - { "/" .join (path_split ) } - '
828835
829836 get_url_with_new_version = lambda v : stringify_source_path (project , v , path )
837+ get_diff_url = lambda v_other : stringify_diff_path (project , version , v_other , path )
830838
831- template = ctx .jinja_env .get_template ('diff.html' )
832-
833- code , code_other = generate_diff (q , project , version , version_other , path )
834839 # Create template context
835840 data = {
836- ** get_layout_template_context (q , ctx , get_url_with_new_version , project , version ),
841+ ** get_layout_template_context (q , ctx , get_url_with_new_version , get_diff_url , project , version ),
837842 ** template_ctx ,
838843
839- 'code' : code ,
840- 'code_other' : code_other ,
844+ 'diff_mode_available' : True ,
845+ 'diff_checked' : True ,
846+ 'diff_exit_url' : stringify_source_path (project , version , path ),
847+
841848 'title_path' : title_path ,
842849 'path' : path ,
843- 'breadcrumb_links' : breadcrumb_links ,
850+ 'breadcrumb_urls' : breadcrumb_urls ,
851+ 'base_url' : diff_base_url ,
844852 }
845853
846854 return (status , template .render (data ))
@@ -923,7 +931,7 @@ def generate_ident_page(ctx: RequestContext, q: Query,
923931 get_url_with_new_version = lambda v : stringify_ident_path (project , v , family , ident )
924932
925933 data = {
926- ** get_layout_template_context (q , ctx , get_url_with_new_version , project , version ),
934+ ** get_layout_template_context (q , ctx , get_url_with_new_version , None , project , version ),
927935
928936 'searched_ident' : ident ,
929937 'current_family' : family ,
@@ -1003,6 +1011,7 @@ def get_application():
10031011 app .add_route ('/{project}/{version}/ident/{ident}' , IdentWithoutFamilyResource ())
10041012 app .add_route ('/{project}/{version}/{family}/ident/{ident}' , IdentResource ())
10051013 app .add_route ('/{project}/{version}/diff/{version_other}/{path:path}' , DiffResource ())
1014+ app .add_route ('/{project}/{version}/diff/{version_other}' , DiffResource ())
10061015
10071016 app .add_route ('/acp' , AutocompleteResource ())
10081017 app .add_route ('/api/ident/{project:project}/{ident:ident}' , ApiIdentGetterResource ())
0 commit comments