Skip to content

Commit

Permalink
Packge dict list and JS modernization (#134)
Browse files Browse the repository at this point in the history
* change format of package_data

* bump version

* Start TS migration / modernization

* such port

* more port

* done with initial port

* 90 errors

* Fixed all the type errors

* docs for codediff.js

* Bundle with webpack

* Rendering something!

* zomg it works!

* checkpoint

* remove some deps, prod mode

* rm generated JS

* ignore generated JS

* Try again

* update some references

* decode

* A few more bug fixes

* Changelog

* Drop top-level package.json

* Publishing instructions
  • Loading branch information
danvk committed Oct 4, 2020
1 parent c443e95 commit e6719d8
Show file tree
Hide file tree
Showing 44 changed files with 4,304 additions and 1,052 deletions.
3 changes: 0 additions & 3 deletions .bowerrc

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -8,6 +8,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
venv/
bin/
build/
develop-eggs/
Expand Down Expand Up @@ -59,5 +60,7 @@ docs/_build/
node_modules
*.ipynb
webdiff/static/components
webdiff/static/js/file_diff.js

wheelhouse
.vscode
8 changes: 8 additions & 0 deletions CHANGELOG
@@ -1,3 +1,11 @@
- 0.15
* Fix `setup.py` configuration error that prevented install on macOS Catalina (#131)
* JavaScript modernization:
* Drop bower in favor of yarn
* Use webpack for bundling
* Convert to TypeScript
* Update to a recent version of React

- 0.14
* Allow the use of hostnames (config option USE_HOSTNAME=True/False)

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
@@ -1,2 +1,3 @@
recursive-include webdiff/templates *
recursive-include webdiff/static *
include README.md
32 changes: 23 additions & 9 deletions README.md
@@ -1,6 +1,5 @@
[![Build Status](https://travis-ci.org/danvk/webdiff.svg?branch=master)](https://travis-ci.org/danvk/webdiff)
git webdiff
===========
# git webdiff

Two-column web-based git difftool.

Expand All @@ -16,13 +15,11 @@ Features include:

![Screenshot of image diffs](http://www.danvk.org/webdiff-images.png)

Installation
------------
## Installation

pip install webdiff

Usage
-----
## Usage

Instead of running "git diff", run:

Expand Down Expand Up @@ -60,13 +57,17 @@ a personal access token for webdiff via github.com → profile → Settings →
Personal access tokens. Make sure to grant all the "repo" privileges.


Development
-----------
## Development

(from an activated virtualenv)

pip install -r requirements.txt
bower install
cd ts
yarn
webpack

Then from the root directory:

./webdiff/app.py testdata/dygraphsjs/{left,right}

or to launch in debug mode:
Expand Down Expand Up @@ -98,4 +99,17 @@ To iterate on the PyPI package, run:
cd /tmp/webdiff-test
pip install webdiff-X.Y.Z.tar.gz

To publish to pypitest:

pip install --upgraede wheel setuptools twine
python setup.py sdist bdist_wheel
twine upload -r testpypi dist/*

And to the real pypi:

twine upload dist/*

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

[oauth]: https://github.com/danvk/webdiff/issues/103
[pypirc]: https://packaging.python.org/specifications/pypirc/
23 changes: 0 additions & 23 deletions package.json

This file was deleted.

13 changes: 10 additions & 3 deletions setup.py
@@ -1,8 +1,15 @@
from setuptools import setup, find_packages


with open('README.md', encoding='utf8') as fh:
long_description = fh.read()


setup(name='webdiff',
version='0.14.0',
version='0.15.0',
description='Two-column web-based git difftool',
long_description=long_description,
long_description_content_type='text/markdown',
author='Dan Vanderkam',
author_email='danvdk@gmail.com',
url='https://github.com/danvk/webdiff/',
Expand All @@ -22,8 +29,8 @@
],
include_package_data=True,
package_data = {
'static': 'webdiff/static/*',
'templates': 'webdiff/templates/*'
'static': ['webdiff/static/*'],
'templates': ['webdiff/templates/*']
},
classifiers=[
'Environment :: Console',
Expand Down
24 changes: 24 additions & 0 deletions ts/AnnotatedImage.tsx
@@ -0,0 +1,24 @@
import React from 'react';
import { ImageDiffProps } from './ImageDiff';
import { ImageMetadata } from './ImageMetadata';
import { SingleImage } from './SingleImage';

export interface Props extends ImageDiffProps {
maxWidth: number | null;
side: 'a' | 'b';
}

export function AnnotatedImage(props: Props) {
const {side} = props;
if (!props.filePair[side]) {
return <span>None</span>;
}

var im = props.filePair[side === 'a' ? 'image_a' : 'image_b'];
return (
<div className={'image-' + side}>
<SingleImage {...props} />
<ImageMetadata image={im} />
</div>
);
}
91 changes: 91 additions & 0 deletions ts/CodeDiff.tsx
@@ -0,0 +1,91 @@
import React from 'react';
import { renderDiff } from './file_diff';

export interface ImageFile {
width: number;
height: number;
num_bytes: number;
}

// XXX this type is probably imprecise. What's a "thick" vs. "thin" diff?
export interface FilePair {
is_image_diff: boolean;
are_same_pixels: boolean;
a: string;
b: string;
type: 'add' | 'delete' | 'move' | 'change'; // XXX check "change"
image_a: ImageFile;
image_b: ImageFile;
idx: number;
diffData?: ImageDiffData;
}

export interface DiffBox {
width: number;
height: number;
left: number;
top: number;
bottom: number;
right: number;
}

export interface ImageDiffData {
diffBounds: DiffBox;
}

// A "no changes" sign which only appears when applicable.
export function NoChanges(props: {filePair: any}) {
const {filePair} = props;
if (filePair.no_changes) {
return <div className="no-changes">(File content is identical)</div>;
} else if (filePair.is_image_diff && filePair.are_same_pixels) {
return (
<div className="no-changes">
Pixels are the same, though file content differs (perhaps the headers are different?)
</div>
);
}
return null;
}

// A side-by-side diff of source code.
export function CodeDiff(props: {filePair: FilePair}) {
const {filePair} = props;
const codediffRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
// Either side can be empty (i.e. an add or a delete), in which case
// getOrNull resolves to null
var getOrNull = async (side: string, path: string) => {
if (!path) return null;
const data = new URLSearchParams();
data.set('path', path);
const response = await fetch(`/${side}/get_contents`, {
method: 'post',
body: data,
});
return response.text();
}

const {a, b} = filePair;
// Do XHRs for the contents of both sides in parallel and fill in the diff.
(async () => {
const [before, after] = await Promise.all([getOrNull('a', a), getOrNull('b', b)]);
// Call out to codediff.js to construct the side-by-side diff.
const codediffEl = codediffRef.current;
if (codediffEl) {
codediffEl.innerHTML = '';
codediffEl.appendChild(renderDiff(a, b, before, after));
}
})().catch(e => {
alert("Unable to get diff!")
});
}, [filePair]);

return (
<div>
<NoChanges filePair={filePair} />
<div ref={codediffRef} key={filePair.idx}>Loading&hellip;</div>
</div>
);
}
40 changes: 40 additions & 0 deletions ts/DiffView.tsx
@@ -0,0 +1,40 @@
import React from 'react';
import { CodeDiff, FilePair } from './CodeDiff';
import { getThickDiff } from './file_diff';
import { ImageDiff } from './ImageDiff';
import { ImageDiffMode } from './ImageDiffModeSelector';

export type PerceptualDiffMode = 'off' | 'bbox' | 'pixels';

export interface Props {
thinFilePair: FilePair;
imageDiffMode: ImageDiffMode;
pdiffMode: PerceptualDiffMode;
changeImageDiffModeHandler: (mode: ImageDiffMode) => void;
changePDiffMode: (pdiffMode: PerceptualDiffMode) => void;
}

export function DiffView(props: Props) {
const [filePair, setFilePair] = React.useState<FilePair | null>(null);

const {thinFilePair} = props;
React.useEffect(() => {
(async () => {
const newFilePair = await getThickDiff(thinFilePair.idx);
setFilePair({
...newFilePair,
idx: thinFilePair.idx,
});
})();
}, [thinFilePair, setFilePair]);

if (!filePair) {
return <div>Loading…</div>;
}

if (filePair.is_image_diff) {
return <ImageDiff filePair={filePair} {...props} />;
} else {
return <CodeDiff filePair={filePair} />;
}
}
50 changes: 50 additions & 0 deletions ts/FileDropdown.tsx
@@ -0,0 +1,50 @@
import React from "react";
import { FilePair } from "./CodeDiff";
import { filePairDisplayName } from "./utils";

export interface Props {
filePairs: FilePair[];
selectedIndex: number;
fileChangeHandler: (newIndex: number) => void;
}

/** A list of files in a dropdown menu. This is more compact with many files. */
export function FileDropdown(props: Props) {
const { filePairs, selectedIndex, fileChangeHandler } = props;

const linkOrNone = (idx: number) => {
if (idx < 0 || idx >= filePairs.length) {
return <i>none</i>;
} else {
return (
<a href="#" onClick={() => fileChangeHandler(idx)}>
{filePairDisplayName(filePairs[idx])}
</a>
);
}
};

const prevLink = linkOrNone(props.selectedIndex - 1);
const nextLink = linkOrNone(props.selectedIndex + 1);

const options = filePairs.map((filePair, idx) => (
<option key={idx} value={idx}>
{filePairDisplayName(filePair)} ({filePair.type})
</option>
));

return (
<div className="file-dropdown">
Prev (k): {prevLink}
<br />
<select
value={selectedIndex}
onChange={(e) => fileChangeHandler(Number(e.target.value))}
>
{options}
</select>
<br />
Next (j): {nextLink}
</div>
);
}
36 changes: 36 additions & 0 deletions ts/FileList.tsx
@@ -0,0 +1,36 @@
import React from "react";
import { FilePair } from "./CodeDiff";
import { filePairDisplayName } from "./utils";

export interface Props {
filePairs: FilePair[];
selectedIndex: number;
fileChangeHandler: (newIndex: number) => void;
}

/**
* A list of all the files. Clicking a non-selected file selects it.
* This view is simpler and generally preferable for short lists of files.
*/
export function FileList(props: Props) {
const { filePairs, selectedIndex, fileChangeHandler } = props;

const lis = filePairs.map((filePair, idx) => {
const displayName = filePairDisplayName(filePair);
const content =
idx !== selectedIndex ? (
<a onClick={() => fileChangeHandler(idx)} href="#">
{displayName}
</a>
) : (
<b>{displayName}</b>
);
return (
<li key={idx}>
<span title={filePair.type} className={`diff ${filePair.type}`} />
{content}
</li>
);
});
return <ul className="file-list">{lis}</ul>;
}

0 comments on commit e6719d8

Please sign in to comment.