Skip to content

Commit f1f5a45

Browse files
committed
diff: Implement diff for navigation
1 parent 1a49850 commit f1f5a45

File tree

7 files changed

+215
-42
lines changed

7 files changed

+215
-42
lines changed

elixir/web.py

Lines changed: 108 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
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 <[email protected]>
@@ -28,7 +29,8 @@
2829
import dataclasses
2930
from collections import OrderedDict, namedtuple
3031
from 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
3234
from urllib import parse
3335
import falcon
3436
import jinja2
@@ -109,7 +111,7 @@ def get_project_error_page(req, resp, exception: ElixirProjectError):
109111

110112
versions_raw = get_versions_cached(query, req.context, project)
111113
get_url_with_new_version = lambda v: stringify_source_path(project, v, '/')
112-
versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, version)
114+
versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, None, version)
113115

114116
if current_version_path[2] is None:
115117
# If details about current version are not available, make base links
@@ -193,6 +195,10 @@ def validate_project_and_version(ctx, project, version):
193195
def get_source_base_url(project: str, version: str) -> str:
194196
return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/source'
195197

198+
def stringify_diff_path(project: str, version: str, version_other: str, path: str) -> str:
199+
return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/diff/' + \
200+
f'{ parse.quote(version_other, safe="") }/{ path }'
201+
196202
# Converts ParsedSourcePath to a string with corresponding URL path
197203
def stringify_source_path(project: str, version: str, path: str) -> str:
198204
if not path.startswith('/'):
@@ -250,14 +256,19 @@ def on_get(self, req, resp, project: str, version: str, path: str):
250256

251257
query.close()
252258

259+
# Returns base url of diff pages
260+
# project and version are assumed to be unquoted
261+
def get_diff_base_url(project: str, version: str, version_other: str) -> str:
262+
return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/diff/{ parse.quote(version_other, safe="") }'
263+
253264
# Handles source URLs without a path, ex. '/u-boot/v2023.10/source'.
254265
# Note lack of trailing slash
255266
class SourceWithoutPathResource(SourceResource):
256267
def on_get(self, req, resp, project: str, version: str):
257268
return super().on_get(req, resp, project, version, '')
258269

259270
class DiffResource:
260-
def on_get(self, req, resp, project: str, version: str, version_other: str, path: str):
271+
def on_get(self, req, resp, project: str, version: str, version_other: str, path: str = ''):
261272
project, version, query = validate_project_and_version(req.context, project, version)
262273
version_other = validate_version(parse.unquote(version_other))
263274
if version_other is None or version_other == 'latest':
@@ -431,7 +442,7 @@ def get_projects(basedir: str) -> list[ProjectEntry]:
431442

432443
# Tuple of version name and URL to chosen resource with that version
433444
# Used to render version list in the sidebar
434-
VersionEntry = namedtuple('VersionEntry', 'version, url')
445+
VersionEntry = namedtuple('VersionEntry', 'version, url, diff_url')
435446

436447
# Takes result of Query.get_versions() and prepares it for the sidebar template.
437448
# Returns an OrderedDict with version information and optionally a triple with
@@ -444,6 +455,7 @@ def get_projects(basedir: str) -> list[ProjectEntry]:
444455
# current_version: string with currently browsed version
445456
def get_versions(versions: OrderedDict[str, OrderedDict[str, str]],
446457
get_url: Callable[[str], str],
458+
get_diff_url: Optional[Callable[[str], str]],
447459
current_version: str) -> Tuple[dict[str, dict[str, list[VersionEntry]]], Tuple[str|None, str|None, str|None]]:
448460

449461
result = OrderedDict()
@@ -455,13 +467,14 @@ def get_versions(versions: OrderedDict[str, OrderedDict[str, str]],
455467
result[major] = OrderedDict()
456468
if minor not in result[major]:
457469
result[major][minor] = []
458-
result[major][minor].append(VersionEntry(v, get_url(v)))
470+
result[major][minor].append(
471+
VersionEntry(v, get_url(v), get_diff_url(v) if get_diff_url is not None else None)
472+
)
459473
if v == current_version:
460474
current_version_path = (major, minor, v)
461475

462476
return result, current_version_path
463477

464-
# Caches get_versions result in a context object
465478
def get_versions_cached(q, ctx, project):
466479
with ctx.versions_cache_lock:
467480
if project not in ctx.versions_cache:
@@ -480,9 +493,9 @@ def get_versions_cached(q, ctx, project):
480493
# project: name of the project
481494
# version: version of the project
482495
def get_layout_template_context(q: Query, ctx: RequestContext, get_url_with_new_version: Callable[[str], str],
483-
project: str, version: str) -> dict[str, Any]:
496+
get_diff_url: Optional[Callable[[str], str]], project: str, version: str) -> dict[str, Any]:
484497
versions_raw = get_versions_cached(q, ctx, project)
485-
versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, version)
498+
versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, get_diff_url, version)
486499

487500
return {
488501
'projects': get_projects(ctx.config.project_dir),
@@ -694,7 +707,7 @@ def get_ident_url(ident, ident_family=None):
694707
# path: path of the file, path to the target in case of symlinks
695708
# url: absolute URL of the file
696709
# size: int, file size in bytes, None for directories and symlinks
697-
DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size')
710+
DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size, cls')
698711

699712
# Returns a list of DirectoryEntry objects with information about files in a directory
700713
# base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash
@@ -705,21 +718,21 @@ def get_directory_entries(q: Query, base_url, tag: str, path: str) -> list[Direc
705718
lines = q.get_dir_contents(tag, path)
706719

707720
for l in lines:
708-
type, name, size, perm = l.split(' ')
721+
type, name, size, perm, blob_id = l.split(' ')
709722
file_path = f"{ path }/{ name }"
710723

711724
if type == 'tree':
712-
dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None))
725+
dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None, None))
713726
elif type == 'blob':
714727
# 120000 permission means it's a symlink
715728
if perm == '120000':
716729
dir_path = path if path.endswith('/') else path + '/'
717730
link_contents = q.get_file_raw(tag, file_path)
718731
link_target_path = os.path.abspath(dir_path + link_contents)
719732

720-
dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size))
733+
dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size, None))
721734
else:
722-
dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size))
735+
dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size, None))
723736

724737
return dir_entries
725738

@@ -774,10 +787,11 @@ def generate_source_page(ctx: RequestContext, q: Query,
774787
title_path = f'{ path_split[-1] } - { "/".join(path_split) } - '
775788

776789
get_url_with_new_version = lambda v: stringify_source_path(project, v, path)
790+
get_diff_url = lambda v_other: stringify_diff_path(project, version, v_other, path)
777791

778792
# Create template context
779793
data = {
780-
**get_layout_template_context(q, ctx, get_url_with_new_version, project, version),
794+
**get_layout_template_context(q, ctx, get_url_with_new_version, get_diff_url, project, version),
781795

782796
'title_path': title_path,
783797
'path': path,
@@ -789,20 +803,84 @@ def generate_source_page(ctx: RequestContext, q: Query,
789803

790804
return (status, template.render(data))
791805

806+
# Returns a list of DirectoryEntry objects with information about files in a directory
807+
# base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash
808+
# tag: requested repository tag
809+
# tag_other: tag to diff with
810+
# path: path to the directory in the repository
811+
def diff_directory_entries(q: Query, base_url, tag: str, tag_other: str, path: str) -> list[DirectoryEntry]:
812+
dir_entries = []
813+
814+
names, names_other = {}, {}
815+
for line in q.get_dir_contents(tag, path):
816+
n = line.split(' ')
817+
names[n[1]] = n
818+
for line in q.get_dir_contents(tag_other, path):
819+
n = line.split(' ')
820+
names_other[n[1]] = n
821+
822+
def dir_sort(name):
823+
if name in names and names[name][0] == 'tree':
824+
return (1, name)
825+
elif name in names_other and names_other[name][0] == 'tree':
826+
return (1, name)
827+
else:
828+
return (2, name)
829+
830+
all_names = set(names.keys())
831+
all_names = all_names.union(names_other.keys())
832+
all_names = sorted(all_names, key=dir_sort)
833+
834+
for name in all_names:
835+
data = names.get(name)
836+
data_other = names_other.get(name)
837+
838+
cls = None
839+
if data is None and data_other is not None:
840+
type, name, size, perm, blob_id = data_other
841+
cls = 'added'
842+
elif data_other is None and data is not None:
843+
type, name, size, perm, blob_id = data
844+
cls = 'removed'
845+
elif data is not None and data_other is not None:
846+
type_old, name, _, _, blob_id = data
847+
type, _, size, perm, blob_id_other = data_other
848+
if blob_id != blob_id_other or type_old != type:
849+
cls = 'changed'
850+
else:
851+
raise Exception("name does not exist " + name)
852+
853+
file_path = f"{ path }/{ name }"
854+
855+
if type == 'tree':
856+
dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None, cls))
857+
elif type == 'blob':
858+
# 120000 permission means it's a symlink
859+
if perm == '120000':
860+
dir_path = path if path.endswith('/') else path + '/'
861+
link_contents = q.get_file_raw(tag, file_path)
862+
link_target_path = os.path.abspath(dir_path + link_contents)
863+
864+
dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size, cls))
865+
else:
866+
dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size, cls))
867+
868+
return dir_entries
869+
792870
# Generates response (status code and optionally HTML) of the `diff` route
793871
def generate_diff_page(ctx: RequestContext, q: Query,
794872
project: str, version: str, version_other: str, path: str) -> tuple[int, str]:
795873

796874
status = falcon.HTTP_OK
797-
source_base_url = get_source_base_url(project, version)
875+
diff_base_url = get_diff_base_url(project, version, version_other)
798876

799877
# Generate breadcrumbs
800878
path_split = path.split('/')[1:]
801879
path_temp = ''
802-
breadcrumb_links = []
880+
breadcrumb_urls = []
803881
for p in path_split:
804882
path_temp += '/'+p
805-
breadcrumb_links.append((p, f'{ source_base_url }{ path_temp }'))
883+
breadcrumb_urls.append((p, f'{ diff_base_url }{ path_temp }'))
806884

807885
type = q.get_file_type(version, path)
808886
type_other = q.get_file_type(version_other, path)
@@ -842,25 +920,18 @@ def generate_warning(type, version):
842920
warning = f'Files are the same in {version} and {version_other}.'
843921
else:
844922
missing_version = version_other if type == 'blob' else version
845-
warning = f'File does not exist or is not a file {missing_version}.'
923+
warning = f'File does not exist, or is not a file in {missing_version}. ({version} displayed)'
846924

847925
template_ctx = {
848926
'code': generate_source(q, project, version if type == 'blob' else version_other, path),
849-
'warning': warning
927+
'warning': warning,
850928
}
851929
template = ctx.jinja_env.get_template('source.html')
852930
else:
853931
raise ElixirProjectError('File not found', f'This file does not exist in {version} nor in {version_other}.',
854932
status=falcon.HTTP_NOT_FOUND,
855933
query=q, project=project, version=version,
856-
extra_template_args={'breadcrumb_links': breadcrumb_links})
857-
858-
if type_other != 'blob':
859-
raise ElixirProjectError('File not found', f'This file is not present in {version_other}.',
860-
status=falcon.HTTP_NOT_FOUND,
861-
query=q, project=project, version=version,
862-
extra_template_args={'breadcrumb_links': breadcrumb_links})
863-
934+
extra_template_args={'breadcrumb_urls': breadcrumb_urls})
864935

865936
# Create titles like this:
866937
# root path: "Linux source code (v5.5.6) - Bootlin"
@@ -874,20 +945,21 @@ def generate_warning(type, version):
874945
title_path = f'{ path_split[-1] } - { "/".join(path_split) } - '
875946

876947
get_url_with_new_version = lambda v: stringify_source_path(project, v, path)
948+
get_diff_url = lambda v_other: stringify_diff_path(project, version, v_other, path)
877949

878-
template = ctx.jinja_env.get_template('diff.html')
879-
880-
code, code_other = generate_diff(q, project, version, version_other, path)
881950
# Create template context
882951
data = {
883-
**get_layout_template_context(q, ctx, get_url_with_new_version, project, version),
952+
**get_layout_template_context(q, ctx, get_url_with_new_version, get_diff_url, project, version),
884953
**template_ctx,
885954

886-
'code': code,
887-
'code_other': code_other,
955+
'diff_mode_available': True,
956+
'diff_checked': True,
957+
'diff_exit_url': stringify_source_path(project, version, path),
958+
888959
'title_path': title_path,
889960
'path': path,
890-
'breadcrumb_links': breadcrumb_links,
961+
'breadcrumb_urls': breadcrumb_urls,
962+
'base_url': diff_base_url,
891963
}
892964

893965
return (status, template.render(data))
@@ -970,7 +1042,7 @@ def generate_ident_page(ctx: RequestContext, q: Query,
9701042
get_url_with_new_version = lambda v: stringify_ident_path(project, v, family, ident)
9711043

9721044
data = {
973-
**get_layout_template_context(q, ctx, get_url_with_new_version, project, version),
1045+
**get_layout_template_context(q, ctx, get_url_with_new_version, None, project, version),
9741046

9751047
'searched_ident': ident,
9761048
'current_family': family,
@@ -1050,6 +1122,7 @@ def get_application():
10501122
app.add_route('/{project}/{version}/ident/{ident}', IdentWithoutFamilyResource())
10511123
app.add_route('/{project}/{version}/{family}/ident/{ident}', IdentResource())
10521124
app.add_route('/{project}/{version}/diff/{version_other}/{path:path}', DiffResource())
1125+
app.add_route('/{project}/{version}/diff/{version_other}', DiffResource())
10531126

10541127
app.add_route('/acp', AutocompleteResource())
10551128
app.add_route('/api/ident/{project:project}/{ident:ident}', ApiIdentGetterResource())

script.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ get_dir()
9595
{
9696
v=`echo $opt1 | version_rev`
9797
git ls-tree -l "$v:`denormalize $opt2`" 2>/dev/null |
98-
awk '{print $2" "$5" "$4" "$1}' |
98+
awk '{print $2" "$5" "$4" "$1" "$3}' |
9999
grep -v ' \.' |
100100
sort -t ' ' -k 1,1r -k 2,2
101101
}

0 commit comments

Comments
 (0)