Skip to content
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

Use git diff for diffs #150

Merged
merged 49 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a02ed3f
NOTES
danvk Sep 6, 2022
0b8915c
Working on diff conversion
danvk Sep 11, 2022
e9d7bf5
Parsing diffs!
danvk Sep 11, 2022
5b6a085
Support num_lines for end skips
danvk Sep 11, 2022
1c6325a
Rename + Change test
danvk Sep 11, 2022
1efeda5
Test with patch-u5
danvk Sep 11, 2022
b4f7e07
LocalFileDiff --> dataclass
danvk Sep 11, 2022
aa36e86
Codes --> dataclass
danvk Sep 11, 2022
9b0fd49
Parsing raw diff output
danvk Sep 11, 2022
d386beb
Use git diff --raw for directory diffs
danvk Sep 11, 2022
05aa4fb
add diff endpoint
danvk Sep 11, 2022
ce0da23
Use git diff ops in webdiff!
danvk Sep 11, 2022
311dab5
Context
danvk Sep 11, 2022
790bf08
Update highlight.js
danvk Sep 11, 2022
923f4e3
post to /diff
danvk Sep 11, 2022
7eb3150
Add an option (-w)
danvk Sep 11, 2022
048647f
Factor out DiffOptionsControl
danvk Sep 11, 2022
384993e
wire up context + diff algorithm
danvk Sep 11, 2022
047a1ca
Add git diff -b as well; unclear if this is different?
danvk Sep 12, 2022
5746e8d
styled diff options panel
danvk Sep 12, 2022
5abe852
thread through some options
danvk Sep 17, 2022
bb3c1aa
wire up styling
danvk Sep 17, 2022
52f4b8d
Wire up configurable highlight.js themes
danvk Sep 17, 2022
57269c9
fix "show -3 lines" bug
danvk Sep 17, 2022
a334a1e
maxLinesForSyntax
danvk Sep 17, 2022
3397e32
Support binary diffs again
danvk Sep 17, 2022
dadcd44
update codediff.js, bugfix
danvk Sep 17, 2022
9ea481c
Fix bug with pure moves
danvk Sep 17, 2022
c0e52ad
add unidiff dependency
danvk Sep 17, 2022
ec57ddc
delete irrelevant test
danvk Sep 17, 2022
f230086
blacken, version bump
danvk Sep 19, 2022
38feb56
Fix issue with git webdiff by adding --no-symlinks
danvk Sep 24, 2022
aecb131
blacken
danvk Sep 24, 2022
0130df6
check for symlinks and inline
danvk May 28, 2024
fc38ae1
we may be in business!
danvk May 28, 2024
5e12369
swap in symlink-y directory
danvk May 28, 2024
090889c
test case for "Show -1 more lines"
danvk May 28, 2024
76ac1ed
no chardiff on last line
danvk May 28, 2024
f42c555
no-trailing-newline test
danvk May 29, 2024
955e2cb
fix bug w/ trailing newlines
danvk May 29, 2024
25db9af
pin Werkzeug
danvk May 29, 2024
635136c
delete TODO
danvk May 29, 2024
118ae79
add long line no space test
danvk May 29, 2024
b3f20fd
pare back test case
danvk May 29, 2024
f9cb6ae
overflow-wrap: anywhere
danvk May 29, 2024
895d2df
webdiff.colors
danvk May 29, 2024
7465a60
add docs on configuration
danvk May 29, 2024
fe1f68c
fix typo
danvk May 29, 2024
93e73bb
add some implementation notes
danvk May 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ webdiff/static/js/file_diff.js

wheelhouse
.vscode
NOTES.md
23 changes: 0 additions & 23 deletions README

This file was deleted.

69 changes: 60 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Two-column web-based git difftool.

Features include:

* Side-by-side (two column) diff view
* Runs in the browser of your choice on any platform.
* Syntax highlighting via highlight.js
Expand Down Expand Up @@ -60,6 +61,33 @@ Make sure you chmod this file to only be readable by yourself. You can generate
a personal access token for webdiff via github.com → profile → Settings →
Personal access tokens. Make sure to grant all the "repo" privileges.

## Configuration

webdiff can be configured via [`git config`][git config]. To change the syntax highlighting theme, for example:

git config webdiff.theme rainbow

(You can find a list of supported themes in the [themes] directory.)

As with any git configuration setting, these can be set globally or per-repo.

Options are:

| Setting | Default | Notes |
| -------------- | ------------- | ------ |
| webdiff.theme | googlecode | Syntax highlighting theme (see [themes] directory). |
| webdiff.port | -1 | Port on which to serve webdiff. Default is random open port. This can be overridden with `--port`. |
| webdiff.maxDiffWidth | 100 | Maximum length of lines in the diff display. After this width, lines will wrap. |
| webdiff.unified | 8 | Lines of context to display by default (`git diff -U`) |
| webdiff.extraDirDiffArgs | "" | Any extra arguments to pass to `git diff` when diffing directories. |
| webdiff.extraFileDiffArgs | "" | Any extra arguments to pass to `git diff` when diffing files. |
| webdiff.openBrowser | true | Whether to automatically open the browser UI when you run webdiff. |
| webdiff.maxLinesForSyntax | 10000 | Maximum lines in file to do syntax highlighting. |
| webdiff.colors.delete | #fee | CSS background color for delete (left) lines |
| webdiff.colors.insert | #efe | CSS background color for insert (right) lines |
| webdiff.colors.charDelete | #fcc | CSS background color for deleted characters in a delete (left) line |
| webdiff.colors.charInsert | #cfc | CSS background color for inserted characters in an insert (right) line |

## Development

python3 -m venv venv
Expand All @@ -76,38 +104,37 @@ Then from the root directory:

or to launch in debug mode:

./test.sh $(pwd)/../testdata/webdiffdiff/{left,right}
./test.sh $(pwd)/testdata/manyfiles/{left,right}

(or any other directory in testdata)

To run the Python tests:

pytest

To run the JavaScript tests:

python -m SimpleHTTPServer
open tests/runner.html

To format the code, run:

./scripts/black.sh
cd ts
yarn prettier

To debug `git webdiff`, run:

WEBDIFF_CONFIG=$(pwd)/testing.cfg ./webdiff/gitwebdiff.py

To iterate on the PyPI package, run:

# from outside the webdiff virtualenv:
pip uninstall webdiff
pip3 uninstall webdiff

# from inside the webdiff virtualenv, adjust for current version
python setup.py sdist
mkdir /tmp/webdiff-test
cp dist/webdiff-X.Y.Z.tar.gz /tmp/webdiff-test
cp dist/webdiff-?.?.?.tar.gz /tmp/webdiff-test

deactivate
cd /tmp/webdiff-test
pip3 install webdiff-X.Y.Z.tar.gz
pip3 install webdiff-?.?.?.tar.gz

To publish to pypitest:

Expand All @@ -121,6 +148,30 @@ And to the real pypi:

See [pypirc][] docs for details on setting up `~/.pypirc`.

## Implementation notes

webdiff doesn't calculate any diffs itself. Instead, it relies on `git diff`. This is possible because `git diff` has a `--no-index` mode that allows it to operate outside of a git repository. Of course, this means that you need to have `git` installed to use webdiff!

When you run `webdiff dir1 dir2`, webdiff runs:

git diff --raw --no-index dir1 dir2

To ask `git` which files are adds, removes, renames and changes. Then, when it's serving the web UI for a particular diff, it runs:

git diff --no-index (diff args) file1 file2

This produces a patch, which is what the web UI renders. (It also needs both full files for syntax highlighting.)

When you run `git webdiff (args)`, it runs:

git difftool -d -x webdiff (args)

This tells `git` to set up two directories and invoke `webdiff leftdir rightdir`.

There's one complication involving symlinks. `git difftool -d` may fill one of the sides (typically the right) with symlinks. This is faster than copying files, but unfortunately `git diff --no-index` does not resolve these symlinks. To make this work, if a directory contains symlinks, webdiff makes a copy of it before diffing. For file diffs, it resolves the symlink before passing it to `git diff --no-index`. The upshot is that you can run `git webdiff`, edit a file, reload the browser window and see the changes.

[pypirc]: https://packaging.python.org/specifications/pypirc/
[Homebrew]: https://brew.sh/
[ImageMagick]: https://imagemagick.org/index.php
[git config]: https://git-scm.com/docs/git-config
[themes]: http://example.com
14 changes: 0 additions & 14 deletions TODO

This file was deleted.

2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ pillow
requests
binaryornot
black
unidiff==0.7.4
Werkzeug==2.2.2
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


setup(name='webdiff',
version='0.16.0',
version='1.0.0',
description='Two-column web-based git difftool',
long_description=long_description,
long_description_content_type='text/markdown',
Expand All @@ -25,7 +25,9 @@
'flask==2.2.2',
'pillow',
'requests',
'PyGithub==1.55'
'PyGithub==1.55',
'unidiff==0.7.4',
'Werkzeug==2.2.2',
],
include_package_data=True,
package_data = {
Expand Down
7 changes: 7 additions & 0 deletions test-gitwebdiff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
export TESTING=true
export DEBUG=true
export WEBDIFF_CONFIG=$(pwd)/testing.cfg
export WEBDIFF_PORT=$(($RANDOM + 10000))
export PYTHONPATH=.
./webdiff/gitwebdiff.py $*
5 changes: 5 additions & 0 deletions testdata/longline-nospace/left/text.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Now it's completely transparent that `requestStatus` will end in "success." It's easy to accidentally produce half-synchronous code with callbacks or raw Promises, but difficult with `async`.footnote:[There's still a more subtle bug in this version: if you call fetchWithCache twice in a row with the same URL, it will issue two requests. How would you fix this?]

// This is the fix, it's three lines longer but I think it adds complexity that's irrelevant to the point that I'm trying to make here. https://www.typescriptlang.org/play?#code/MYewdgzgLgBA+sAhsAFgUwFwwN4G0CuATgDZbSECWYA5gLpYAKhIAthRGgDwBKaEADuA4A+AL4wAvDlEBuAFCIIATzDAYAM3yqoFcBrRRUAdQpQUAYWToAFEVIxyVagEpGzNhx59BkNMJxyMDAU6jC2JMFg8EioaM4BQUGEBkRRCFZoBCS08kGigTCgkLDqBqgMkvqGKOHEzrnRGVnEtJWl1QwNyVCpVeXy+YoqapraulHtqAAqaAAeUCZmlrG1ZFCUNK4wTKzsXI40-tgFRdAwyQJCaJWIAO6Ipn0oixYZtfUF3b0XPhwAdFA5lBrB98nJiAZzmgAI74PhQADKUEQPQgWAA5MQQIgACZOdEwAA+MHREHwwGAfAgBOJ6LQhGYhHR8iGqg0WmAOj01AMAFUOIRbAKAJI4tYbFwJKGw+FIlH4CCVTHYvE0ZknISwfjMdQUCE3e6PSYoGbzF7LGwAAwA9Ar6daACTYO2EUWiS0fJIwuHQOWopVkilU9X5IA

Note that if you return a Promise from an `async` function, it will not get wrapped in another Promise: the return type will be `Promise<T>` rather than `Promise<Promise<T>>`. Again, TypeScript will help you build an intuition for this:
5 changes: 5 additions & 0 deletions testdata/longline-nospace/right/text.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Now it's completely transparent that `requestStatus` will end in "success." It's easy to accidentally produce half-synchronous code with callbacks or raw Promises, but difficult with `async`.footnote:[There's still a more subtle bug in this version: if you call ++fetchWithCache++ twice in a row with the same URL, it will issue two requests. How would you fix this?]

// This is the fix, it's three lines longer but I think it adds complexity that's irrelevant to the point that I'm trying to make here. https://www.typescriptlang.org/play?#code/MYewdgzgLgBA+sAhsAFgUwFwwN4G0CuATgDZbSECWYA5gLpYAKhIAthRGgDwBKaEADuA4A+AL4wAvDlEBuAFCIIATzDAYAM3yqoFcBrRRUAdQpQUAYWToAFEVIxyVagEpGzNhx59BkNMJxyMDAU6jC2JMFg8EioaM4BQUGEBkRRCFZoBCS08kGigTCgkLDqBqgMkvqGKOHEzrnRGVnEtJWl1QwNyVCpVeXy+YoqapraulHtqAAqaAAeUCZmlrG1ZFCUNK4wTKzsXI40-tgFRdAwyQJCaJWIAO6Ipn0oixYZtfUF3b0XPhwAdFA5lBrB98nJiAZzmgAI74PhQADKUEQPQgWAA5MQQIgACZOdEwAA+MHREHwwGAfAgBOJ6LQhGYhHR8iGqg0WmAOj01AMAFUOIRbAKAJI4tYbFwJKGw+FIlH4CCVTHYvE0ZknISwfjMdQUCE3e6PSYoGbzF7LGwAAwA9Ar6daACTYO2EUWiS0fJIwuHQOWopVkilU9X5IA

Note that if you return a Promise from an `async` function, it will not get wrapped in another Promise: the return type will be `Promise<T>` rather than ++Promise&#x200b;&lt;Promise&lt;T&gt;&gt;++. Again, TypeScript will help you build an intuition for this:
4 changes: 4 additions & 0 deletions testdata/no-trailing-newline/left/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This is a file
It has four lines
and a
trailing newline
4 changes: 4 additions & 0 deletions testdata/no-trailing-newline/right/add.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This is a new file
with four lines,
but not a trailing
newline in sight
4 changes: 4 additions & 0 deletions testdata/no-trailing-newline/right/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This is a file
It has still four lines
and a
trailing new line
5 changes: 5 additions & 0 deletions testdata/rename+change/left/huckfinn.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CHAPTER I.

You don’t know about me without you have read a book by the name of The Adventures of Tom Sawyer; but that ain’t no matter. That book was made by Mr. Mark Twain, and he told the truth, mainly. There was things which he stretched, but mainly he told the truth. That is nothing. I never seen anybody but lied one time or another, without it was Aunt Polly, or the widow, or maybe Mary. Aunt Polly—Tom’s Aunt Polly, she is—and Mary, and the Widow Douglas is all told about in that book, which is mostly a true book, with some stretchers, as I said before.

Now the way that the book winds up is this: Tom and me found the money that the robbers hid in the cave, and it made us rich. We got six thousand dollars apiece—all gold. It was an awful sight of money when it was piled up. Well, Judge Thatcher he took it and put it out at interest, and it fetched us a dollar a day apiece all the year round—more than a body could tell what to do with. The Widow Douglas she took me for her son, and allowed she would sivilize me; but it was rough living in the house all the time, considering how dismal regular and decent the widow was in all her ways; and so when I couldn’t stand it no longer I lit out. I got into my old rags and my sugar-hogshead again, and was free and satisfied. But Tom Sawyer he hunted me up and said he was going to start a band of robbers, and I might join if I would go back to the widow and be respectable. So I went back.
5 changes: 5 additions & 0 deletions testdata/rename+change/right/huckfinn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CHAPTER I

You don’t know about me without you have read a book by the name of The Adventures of Tom Sawyer; but that ain’t no matter. That book was made by Mr. Mark Twain, and he told the truth, mainly. There was things which he stretched, but mainly he told the truth. That is nothing. I never seen anybody but lied one time or another, without it was Aunt Polly, or the widow, or maybe Mary. Aunt Polly—Tom’s Aunt Polly, she is—and Mary, and the Widow Douglas is all told about in that book, which is mostly a true book, with some stretchers, as I said before.

Now the way that the book winds up is this: Tom and me found the money that the robbers hid in the cave, and it made us rich. We got six thousand dollars apiece—all gold. It was an awful sight of money when it was piled up. Well, Judge Thatcher he took it and put it out at interest, and it fetched us a dollar a day apiece all the year round—more than a body could tell what to do with. The Widow Douglas she took me for her son, and allowed she would civilize me; but it was rough living in the house all the time, considering how dismal regular and decent the widow was in all her ways; and so when I couldn’t stand it no longer I lit out. I got into my old rags and my sugar-hogshead again, and was free and satisfied. But Tom Sawyer he hunted me up and said he was going to start a band of robbers, and I might join if I would go back to the widow and be respectable. So I went back.
34 changes: 34 additions & 0 deletions testdata/unified/dygraphs-patch-u5.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
diff --git a/tmp/before.js b/tmp/after.js
index 63a4828..cea3ddd 100644
--- a/tmp/before.js
+++ b/tmp/after.js
@@ -1,24 +1,25 @@
/**
* Convert a JS date to a string appropriate to display on an axis that
- * is displaying values at the stated granularity.
* @param {Date} date The date to format
* @param {number} granularity One of the Dygraph granularity constants
* @return {string} The formatted date
* @private
*/
Dygraph.dateAxisFormatter = function(date, granularity) {
if (granularity >= Dygraph.DECADAL) {
- return '' + date.getFullYear();
+ return 'xx' + date.getFullYear();
} else if (granularity >= Dygraph.MONTHLY) {
return Dygraph.SHORT_MONTH_NAMES_[date.getMonth()] + ' ' + date.getFullYear();
} else {
- var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+ var frac = date.getHours() * 3600 + date.getMinutes() * 60 +
+ date.getSeconds() + date.getMilliseconds();
if (frac === 0 || granularity >= Dygraph.DAILY) {
// e.g. '21Jan' (%d%b)
var nd = new Date(date.getTime() + 3600*1000);
return Dygraph.zeropad(nd.getDate()) + Dygraph.SHORT_MONTH_NAMES_[nd.getMonth()];
+ return "something else";
} else {
return Dygraph.hmsString_(date.getTime());
}
}
};
\ No newline at end of file
31 changes: 31 additions & 0 deletions testdata/unified/dygraphs-patch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
diff --git a/tmp/before.js b/tmp/after.js
index 63a4828..cea3ddd 100644
--- a/tmp/before.js
+++ b/tmp/after.js
@@ -1,6 +1,5 @@
/**
* Convert a JS date to a string appropriate to display on an axis that
- * is displaying values at the stated granularity.
* @param {Date} date The date to format
* @param {number} granularity One of the Dygraph granularity constants
* @return {string} The formatted date
@@ -8,15 +7,17 @@
*/
Dygraph.dateAxisFormatter = function(date, granularity) {
if (granularity >= Dygraph.DECADAL) {
- return '' + date.getFullYear();
+ return 'xx' + date.getFullYear();
} else if (granularity >= Dygraph.MONTHLY) {
return Dygraph.SHORT_MONTH_NAMES_[date.getMonth()] + ' ' + date.getFullYear();
} else {
- var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
+ var frac = date.getHours() * 3600 + date.getMinutes() * 60 +
+ date.getSeconds() + date.getMilliseconds();
if (frac === 0 || granularity >= Dygraph.DAILY) {
// e.g. '21Jan' (%d%b)
var nd = new Date(date.getTime() + 3600*1000);
return Dygraph.zeropad(nd.getDate()) + Dygraph.SHORT_MONTH_NAMES_[nd.getMonth()];
+ return "something else";
} else {
return Dygraph.hmsString_(date.getTime());
}
9 changes: 9 additions & 0 deletions testdata/unified/manyfiles.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:100644 100644 f00c965 f00c965 R100 testdata/manyfiles/left/d.txt testdata/manyfiles/right/a.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/b.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/c.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/e.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/f.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/g.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/h.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/i.txt
:100644 100644 0000000 0000000 M testdata/manyfiles/left/j.txt
1 change: 1 addition & 0 deletions testdata/unified/rename+change.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:100644 100644 4dc9e64 ccb4941 R090 testdata/rename+change/left/huckfinn.txt testdata/rename+change/right/huckfinn.md
50 changes: 0 additions & 50 deletions tests/pair_test.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,4 @@
import unittest

from webdiff import util
from webdiff import diff
from webdiff import dirdiff


def test_pairing():
a_files = [
'app.py',
'TODO',
'static/js/file_diff.js',
'static/jsdifflib/diffview.css',
'static/jsdifflib/diffview.js',
'templates/heartbeat.html',
]

b_files = [
'app.py',
'testing.cfg',
'TODO',
'static/js/file_diff.js',
'static/jsdifflib/diffview.css',
'static/jsdifflib/diffview.js',
'templates/heartbeat.html',
]

pairs = dirdiff.pair_files(a_files, b_files)
pairs.sort()

assert [
('', 'testing.cfg'),
('TODO', 'TODO'),
('app.py', 'app.py'),
('static/js/file_diff.js', 'static/js/file_diff.js'),
('static/jsdifflib/diffview.css', 'static/jsdifflib/diffview.css'),
('static/jsdifflib/diffview.js', 'static/jsdifflib/diffview.js'),
('templates/heartbeat.html', 'templates/heartbeat.html'),
] == pairs


def test_pairing_with_move():
testdir = 'testdata/renamedfile'
diffs = dirdiff.diff('%s/left/dir' % testdir, '%s/right/dir' % testdir)
assert [
{
'a': 'file.json',
'b': 'renamed.json',
'type': 'move',
}
] == [diff.get_thin_dict(d) for d in diffs]


class TinyDiff(object):
Expand Down