Skip to content

Diff view #396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions elixir/diff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import os
from typing import Generator, Optional, Tuple, List
from pygments.formatters import HtmlFormatter
from .query import Query
from .web_utils import DirectoryEntry

from elixir.query import DiffEntry

PygmentsSource = Generator[Tuple[int, str], None, None]

# Wraps pygments HtmlFormatter to create a single diff pane, depending on `left` argument
class DiffFormater(HtmlFormatter):
def __init__(self, diff: List[DiffEntry], left: bool, *args, **kwargs):
self.diff = diff
self.left = left
super().__init__(*args[2:], **kwargs)

# Wraps pygments line (1 if increase line number, html) in span tag with css_class
def mark_line(self, line: Tuple[int, str], css_class: str) -> PygmentsSource:
yield line[0], f'<span class="{css_class}">{line[1]}</span>'

# Wraps num pygments lines from source in span tag with css_class
def mark_lines(self, source: PygmentsSource, num: int, css_class: str) -> PygmentsSource:
i = 0
while i < num:
try:
t, line = next(source)
except StopIteration:
break
if t == 1:
yield t, f'<span class="{css_class}">{line}</span>'
i += 1
else:
yield t, line

# Yields num empty lines
def yield_empty(self, num: int) -> PygmentsSource:
for _ in range(num):
yield 0, '<span class="diff-line">&nbsp;\n</span>'

# Returns diff entry, number of the entry after it and the start line
# of the change in the current pane (left or right).
def get_next_diff_line(self, diff_num: int, next_diff_line: Optional[int]) \
-> Tuple[Optional[DiffEntry], int, Optional[int]]:

next_diff = self.diff[diff_num] if len(self.diff) > diff_num else None

if next_diff is not None:
if self.left and next_diff.type in ('-', '+'):
next_diff_line = next_diff.left_start
elif next_diff.type in ('-', '+'):
next_diff_line = next_diff.right_start
elif self.left and next_diff.type == '=':
next_diff_line = next_diff.left_start
elif next_diff.type == '=':
next_diff_line = next_diff.right_start
else:
raise Exception("invalid next diff mode")

return next_diff, diff_num+1, next_diff_line

# Wraps Pygments source generator in diff generator
def wrap_diff(self, source: PygmentsSource):
next_diff, diff_num, next_diff_line = self.get_next_diff_line(0, None)

linenum = 0

while True:
try:
line = next(source)
except StopIteration:
break

# If processing line that begins current diff entry
if linenum == next_diff_line:
if next_diff is not None:
# Yield empty lines in left pane for new lines in right file
if self.left and next_diff.type == '+':
yield from self.yield_empty(next_diff.right_changed)
yield line
linenum += 1
# Yield green source lines in right pane for new lines in right file
elif next_diff.type == '+':
yield from self.mark_line(line, 'line-added')
yield from self.mark_lines(source, next_diff.right_changed-1, 'line-added')
linenum += next_diff.right_changed
# Yield red source lines in left pane for removed lines in right file
elif self.left and next_diff.type == '-':
yield from self.mark_line(line, 'line-removed')
yield from self.mark_lines(source, next_diff.right_changed-1, 'line-removed')
linenum += next_diff.right_changed
# Yield empty lines in right pane for removed lines in right file
elif next_diff.type == '-':
yield from self.yield_empty(next_diff.right_changed)
yield line
linenum += 1
# Yield highlighted source lines or empty lines in left/right pane for lines changed between files
elif next_diff.type == '=':
total = max(next_diff.left_changed, next_diff.right_changed)
to_print = next_diff.left_changed if self.left else next_diff.right_changed
yield from self.mark_line(line, 'line-removed' if self.left else 'line-added')
yield from self.mark_lines(source, to_print-1, 'line-removed' if self.left else 'line-added')
yield from self.yield_empty(total-to_print)
linenum += to_print
else:
yield line
linenum += 1

next_diff, diff_num, next_diff_line = self.get_next_diff_line(diff_num, next_diff_line)
# Otherwise just return the line
else:
yield line
linenum += 1

def wrap(self, source):
return super().wrap(self.wrap_diff(source))

def format_diff(filename: str, diff, code: str, code_other: str) -> Tuple[str, str]:
import pygments
import pygments.lexers
import pygments.formatters
from pygments.lexers.asm import GasLexer
from pygments.lexers.r import SLexer

try:
lexer = pygments.lexers.guess_lexer_for_filename(filename, code)
if filename.endswith('.S') and isinstance(lexer, SLexer):
lexer = GasLexer()
except pygments.util.ClassNotFound:
lexer = pygments.lexers.get_lexer_by_name('text')

lexer.stripnl = False

formatter = DiffFormater(
diff,
True,
# Adds line numbers column to output
linenos='inline',
# Wraps line numbers in link (a) tags
anchorlinenos=True,
# Wraps each line in a span tag with id='codeline-{line_number}'
linespans='codeline',
)

formatter_other = DiffFormater(
diff,
False,
# Adds line numbers column to output
linenos='inline',
# Wraps line numbers in link (a) tags
anchorlinenos=True,
# Wraps each line in a span tag with id='codeline-{line_number}'
linespans='codeline',
)

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

# Returns a list of DirectoryEntry objects with information about changes between version.
# base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash
# tag: requested repository tag
# tag_other: tag to diff with
# path: path to the directory in the repository
def diff_directory_entries(q: Query, base_url, tag: str, tag_other: str, path: str) -> list[DirectoryEntry]:
dir_entries = []

# Fetch list of names in both directories
names, names_other = {}, {}
for line in q.get_dir_contents(tag, path):
n = line.split(' ')
names[n[1]] = n
for line in q.get_dir_contents(tag_other, path):
n = line.split(' ')
names_other[n[1]] = n

# Used to sort names - directories first, files second
def dir_sort(name):
if name in names and names[name][0] == 'tree':
return (1, name)
elif name in names_other and names_other[name][0] == 'tree':
return (1, name)
else:
return (2, name)

# Create a sorted list of all unique filenames from both versions
all_names = set(names.keys())
all_names = all_names.union(names_other.keys())
all_names = sorted(all_names, key=dir_sort)

for name in all_names:
data = names.get(name)
data_other = names_other.get(name)

diff_cls = None

# Added if file only in right version
if data is None and data_other is not None:
type, name, size, perm, blob_id = data_other
diff_cls = 'added'
# Removed if file only in left version
elif data_other is None and data is not None:
type, name, size, perm, blob_id = data
diff_cls = 'removed'
# If file in both versions
elif data is not None and data_other is not None:
type_old, name, _, _, blob_id = data
type, _, size, perm, blob_id_other = data_other
# changed only if blob id is different
if blob_id != blob_id_other or type_old != type:
diff_cls = 'changed'
else:
raise Exception("name does not exist " + name)

file_path = f"{ path }/{ name }"

if type == 'tree':
dir_entries.append(DirectoryEntry('tree', name, file_path,
f"{ base_url }{ file_path }", None, diff_cls))
elif type == 'blob':
# 120000 permission means it's a symlink
if perm == '120000':
dir_path = path if path.endswith('/') else path + '/'
link_contents = q.get_file_raw(tag, file_path)
link_target_path = os.path.abspath(dir_path + link_contents)

dir_entries.append(DirectoryEntry('symlink', name, link_target_path,
f"{ base_url }{ link_target_path }", size, diff_cls))
else:
dir_entries.append(DirectoryEntry('blob', name, file_path,
f"{ base_url }{ file_path }", size, diff_cls))

return dir_entries

4 changes: 2 additions & 2 deletions elixir/filters/configin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for Config.in includes
# source "path/file"
Expand All @@ -23,7 +23,7 @@ def keep_configin(m):
def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_configin(m):
w = self.configin[decode_number(m.group(1)) - 1]
return f'<a href="{ ctx.get_absolute_source_url(w) }">{ w }</a>'
return format_source_link(ctx.get_absolute_source_url(w), w)

return re.sub('__KEEPCONFIGIN__([A-J]+)', replace_configin, html, flags=re.MULTILINE)

5 changes: 2 additions & 3 deletions elixir/filters/cppinc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link

# Filters for cpp includes like these:
# #include "file"
Expand All @@ -24,8 +24,7 @@ def keep_cppinc(m):
def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_cppinc(m):
w = self.cppinc[decode_number(m.group(1)) - 1]
url = ctx.get_relative_source_url(w)
return f'<a href="{ url }">{ w }</a>'
return format_source_link(ctx.get_relative_source_url(w), w)

return re.sub('__KEEPCPPINC__([A-J]+)', replace_cppinc, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/cpppathinc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link

# Filters for cpp includes like these:
# #include <file>
Expand Down Expand Up @@ -36,7 +36,7 @@ def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_cpppathinc(m):
w = self.cpppathinc[decode_number(m.group(1)) - 1]
path = f'/include/{ w }'
return f'<a href="{ ctx.get_absolute_source_url(path) }">{ w }</a>'
return format_source_link(ctx.get_absolute_source_url(path), w)

return re.sub('__KEEPCPPPATHINC__([A-J]+)', replace_cpppathinc, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/dtsi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches
from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link

# Filters for dts includes as follows:
# Replaces include directives in dts/dtsi files with links to source
Expand All @@ -24,7 +24,7 @@ def keep_dtsi(m):
def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_dtsi(m):
w = self.dtsi[decode_number(m.group(1)) - 1]
return f'<a href="{ ctx.get_relative_source_url(w) }">{ w }</a>'
return format_source_link(ctx.get_relative_source_url(w), w)

return re.sub('__KEEPDTSI__([A-J]+)', replace_dtsi, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/kconfig.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, encode_number, decode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, encode_number, decode_number, filename_without_ext_matches, format_source_link

# Filters for Kconfig includes
# Replaces KConfig includes (source keyword) with links to included files
Expand All @@ -24,7 +24,7 @@ def keep_kconfig(m):
def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_kconfig(m):
w = self.kconfig[decode_number(m.group(1)) - 1]
return f'<a href="{ ctx.get_absolute_source_url(w) }">{ w }</a>'
return format_source_link(ctx.get_absolute_source_url(w), w)

return re.sub('__KEEPKCONFIG__([A-J]+)', replace_kconfig, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/makefiledir.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from os.path import dirname
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for Makefile directory includes as follows:
# obj-$(VALUE) += dir/
Expand Down Expand Up @@ -39,7 +39,7 @@ def replace_makefiledir(m):

fpath = f'{ filedir }{ w }/Makefile'

return f'<a href="{ ctx.get_absolute_source_url(fpath) }">{ w }/</a>'
return format_source_link(ctx.get_absolute_source_url(fpath), w)

return re.sub('__KEEPMAKEFILEDIR__([A-J]+)/', replace_makefiledir, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/makefiledtb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from os.path import dirname
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for Makefile file includes like these:
# dtb-y += file.dtb
Expand Down Expand Up @@ -30,7 +30,7 @@ def replace_makefiledtb(m):
filedir += '/'

npath = f'{ filedir }{ w }.dts'
return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }.dtb</a>'
return format_source_link(ctx.get_absolute_source_url(npath), w+'.dtb')

return re.sub('__KEEPMAKEFILEDTB__([A-J]+)\.dtb', replace_makefiledtb, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/makefilefile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from os.path import dirname
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for files listed in Makefiles
# path/file
Expand Down Expand Up @@ -38,7 +38,7 @@ def replace_makefilefile(m):
filedir += '/'

npath = filedir + w
return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }</a>'
return format_source_link(ctx.get_absolute_source_url(npath), w)

return re.sub('__KEEPMAKEFILEFILE__([A-J]+)', replace_makefilefile, html, flags=re.MULTILINE)

4 changes: 2 additions & 2 deletions elixir/filters/makefileo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from os.path import dirname
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for Makefile file includes like these:
# file.o
Expand Down Expand Up @@ -30,7 +30,7 @@ def replace_makefileo(m):
filedir += '/'

npath = f'{ filedir }{ w }.c'
return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }.o</a>'
return format_source_link(ctx.get_absolute_source_url(npath), w+'.o')

return re.sub('__KEEPMAKEFILEO__([A-J]+)\.o', replace_makefileo, html, flags=re.MULTILINE)

5 changes: 2 additions & 3 deletions elixir/filters/makefilesrctree.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches
from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link

# Filters for files listed in Makefiles using $(srctree)
# $(srctree)/Makefile
Expand Down Expand Up @@ -27,8 +27,7 @@ def keep_makefilesrctree(m):
def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str:
def replace_makefilesrctree(m):
w = self.makefilesrctree[decode_number(m.group(1)) - 1]
url = ctx.get_absolute_source_url(w)
return f'<a href="{ url }">$(srctree)/{ w }</a>'
return format_source_link(ctx.get_absolute_source_url(w), f'$(srctree)/{ w }')

return re.sub('__KEEPMAKEFILESRCTREE__([A-J]+)', replace_makefilesrctree, html, flags=re.MULTILINE)

Loading