Skip to content

Commit 3769656

Browse files
committed
diff: Implement diff for navigation
1 parent 7d7de61 commit 3769656

9 files changed

Lines changed: 220 additions & 43 deletions

File tree

elixir/diff.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import os
12
from typing import Tuple
23
from pygments.formatters import HtmlFormatter
4+
from .query import Query
5+
from .web_utils import DirectoryEntry
36

47
class DiffFormater(HtmlFormatter):
58
def __init__(self, diff, left: bool, *args, **kwargs):
@@ -132,3 +135,67 @@ def format_diff(filename: str, diff, code: str, code_other: str) -> Tuple[str, s
132135

133136
return pygments.highlight(code, lexer, formatter), pygments.highlight(code_other, lexer, formatter_other)
134137

138+
# Returns a list of DirectoryEntry objects with information about files in a directory
139+
# base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash
140+
# tag: requested repository tag
141+
# tag_other: tag to diff with
142+
# path: path to the directory in the repository
143+
def diff_directory_entries(q: Query, base_url, tag: str, tag_other: str, path: str) -> list[DirectoryEntry]:
144+
dir_entries = []
145+
146+
names, names_other = {}, {}
147+
for line in q.get_dir_contents(tag, path):
148+
n = line.split(' ')
149+
names[n[1]] = n
150+
for line in q.get_dir_contents(tag_other, path):
151+
n = line.split(' ')
152+
names_other[n[1]] = n
153+
154+
def dir_sort(name):
155+
if name in names and names[name][0] == 'tree':
156+
return (1, name)
157+
elif name in names_other and names_other[name][0] == 'tree':
158+
return (1, name)
159+
else:
160+
return (2, name)
161+
162+
all_names = set(names.keys())
163+
all_names = all_names.union(names_other.keys())
164+
all_names = sorted(all_names, key=dir_sort)
165+
166+
for name in all_names:
167+
data = names.get(name)
168+
data_other = names_other.get(name)
169+
170+
diff_cls = None
171+
if data is None and data_other is not None:
172+
type, name, size, perm, blob_id = data_other
173+
diff_cls = 'added'
174+
elif data_other is None and data is not None:
175+
type, name, size, perm, blob_id = data
176+
diff_cls = 'removed'
177+
elif data is not None and data_other is not None:
178+
type_old, name, _, _, blob_id = data
179+
type, _, size, perm, blob_id_other = data_other
180+
if blob_id != blob_id_other or type_old != type:
181+
diff_cls = 'changed'
182+
else:
183+
raise Exception("name does not exist " + name)
184+
185+
file_path = f"{ path }/{ name }"
186+
187+
if type == 'tree':
188+
dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None, diff_cls))
189+
elif type == 'blob':
190+
# 120000 permission means it's a symlink
191+
if perm == '120000':
192+
dir_path = path if path.endswith('/') else path + '/'
193+
link_contents = q.get_file_raw(tag, file_path)
194+
link_target_path = os.path.abspath(dir_path + link_contents)
195+
196+
dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size, diff_cls))
197+
else:
198+
dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size, diff_cls))
199+
200+
return dir_entries
201+

elixir/web.py

Lines changed: 44 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 <mikael.bouillot@bootlin.com>
@@ -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
@@ -42,7 +44,7 @@
4244
from .query import get_query
4345
from .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

4749
VERSION_CACHE_DURATION_SECONDS = 2 * 60 # 2 minutes
4850
ADD_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):
194196
def 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
198204
def 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
256267
class SourceWithoutPathResource(SourceResource):
257268
def on_get(self, req, resp, project: str, version: str):
258269
return super().on_get(req, resp, project, version, '')
259270

260271
class 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
446457
def 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
466479
def 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
483496
def 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())

elixir/web_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,6 @@ def convert(self, value: str) -> str|None:
8888
# path: path of the file, path to the target in case of symlinks
8989
# url: absolute URL of the file
9090
# size: int, file size in bytes, None for directories and symlinks
91-
DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size')
91+
# diff: file state in a diff - "added", "removed", "changed" or None
92+
DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size, diff')
9293

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)