gitlatexdiff-original 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitlatexdiff_original-0.2.0/PKG-INFO +159 -0
- gitlatexdiff_original-0.2.0/README.md +138 -0
- gitlatexdiff_original-0.2.0/pyproject.toml +61 -0
- gitlatexdiff_original-0.2.0/setup.cfg +4 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff/__init__.py +25 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff/flatten_latex.py +198 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff/make_diff.py +375 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/PKG-INFO +159 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/SOURCES.txt +11 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/dependency_links.txt +1 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/entry_points.txt +3 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/requires.txt +5 -0
- gitlatexdiff_original-0.2.0/src/gitlatexdiff_original.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitlatexdiff-original
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Make diff of two versions of a LaTeX document in a Git repo
|
|
5
|
+
Author-email: bjhend <developer@bjhend.de>
|
|
6
|
+
Maintainer-email: bjhend <developer@bjhend.de>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Project-URL: Repository, https://github.com/bjhend/gitlatexdiff
|
|
9
|
+
Project-URL: Issues, https://github.com/bjhend/gitlatexdiff/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/bjhend/gitlatexdiff/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: git,latex,diff,tool
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: beartype
|
|
17
|
+
Requires-Dist: mkdocs
|
|
18
|
+
Requires-Dist: mkdocstrings[python]
|
|
19
|
+
Requires-Dist: pymdown-extensions
|
|
20
|
+
Requires-Dist: icecream
|
|
21
|
+
|
|
22
|
+
# Git-LaTeX-Diff Original
|
|
23
|
+
|
|
24
|
+
Make a rendered diff of two versions of a LaTeX document.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### License
|
|
28
|
+
|
|
29
|
+
See [License](LICENSE.md)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Changelog
|
|
33
|
+
|
|
34
|
+
See [Changelog](CHANGELOG.md)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Purpose
|
|
38
|
+
|
|
39
|
+
[*latexdiff*](https://www.ctan.org/pkg/latexdiff) is a LaTeX tool to create a diff of two LaTeX documents, which shows deletions and additions as red strike-through text and additions as blue underlined text when compiled to PDF. However, *latexdiff* has some major limitations.
|
|
40
|
+
|
|
41
|
+
To overcome the limitations, this Python script extends *latexdiff* in several ways:
|
|
42
|
+
|
|
43
|
+
* It works with a Git repo such that it compares the current state or a given commit with an earlier commit
|
|
44
|
+
* It resolves `\include` and `\input` commands like LaTeX does
|
|
45
|
+
* It calls `pdflatex` to render the final PDF
|
|
46
|
+
|
|
47
|
+
In addition the `\include` and `\input` resolving itself can be called as standalone script.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Caveats
|
|
52
|
+
|
|
53
|
+
* While `\include` and `\input` are resolved from the respective git revisions, other includes like figures are resolved when compiling the diff. This is done in the new revision. So if the old version includes figures that are missing or renamed in the new revision they will be missing in the diff PDF as well.
|
|
54
|
+
* There is no diff of the bibliography and other generated parts.
|
|
55
|
+
* When using uncommitted changes as new version the rendering has to take place in the documents work directory. This may leave temporary files and have other unexpected side effects. However, if everything is committed or dedicated Git revisions are compared all processing is done in temporary directories that are cleaned up.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
### Prerequesites
|
|
60
|
+
|
|
61
|
+
* LaTeX must be installed including the tools
|
|
62
|
+
* `pdflatex`
|
|
63
|
+
* `latexdiff`
|
|
64
|
+
* Python3 is available with at least the version set in `pyproject.toml`
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
The easiest way is to apply the Python tool `uv`, which we describe here. Likely `poetry` will work as well.
|
|
71
|
+
|
|
72
|
+
Change to the directory where you cloned the gitlatexdiff project and call
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
to install all dependencies and create a virtual environment.
|
|
79
|
+
|
|
80
|
+
To run the script call
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run gitlatexdiff <options>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To run the script to flatten a LeTeX file standalone call
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run flattenlatex <options>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#### Tips
|
|
94
|
+
|
|
95
|
+
If the diff sources cannot be compiled check the log file for problems with `\DIF...` commands and see which original LaTeX command caused it. Then you may exclude that command from the diff with for example:
|
|
96
|
+
|
|
97
|
+
`-l 'append-textcmd=hint.*,todo' 'exclude-textcmd=title,.*section,chapter'`
|
|
98
|
+
|
|
99
|
+
Here, LaTeX commands `\title`, `\chapter`, and all ending in `section` are excluded, so diffs in these commands are not marked in the output.
|
|
100
|
+
|
|
101
|
+
Note, that in this case the `'append-textcmd=hint.*,todo'` is the default for option `-l`, which needs to be set explicitely if `-l` is given.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### Options
|
|
105
|
+
|
|
106
|
+
Call `gitlatexdiff` with option `--help` to get the current list of command line options and their defaults if applicable.
|
|
107
|
+
|
|
108
|
+
#### Mandatory options
|
|
109
|
+
|
|
110
|
+
* `-m`, `--main`: Name of the main LaTeX file whose versions should be compared. It has to reside in the respective Git repository containing the versions to compare. May be given with path if `gitlatexdiff` is called from outside the LaTeX project directory.
|
|
111
|
+
|
|
112
|
+
#### Optional
|
|
113
|
+
|
|
114
|
+
All other command line options are optional.
|
|
115
|
+
|
|
116
|
+
Call `gitlatexdiff --help` to see the defaults of the following options.
|
|
117
|
+
|
|
118
|
+
* `-n`, `--new-rev`: Newer revision to compare with. If not given the current state of the work files is used, which will be the HEAD revision if all files are committed or else the work files.
|
|
119
|
+
* `-o`, `--old-rev`: Older revision to compare with. If not given either the revision before `--new-rev` is used or the HEAD revision if `--new-rev` is also not given and there are uncommitted changes.
|
|
120
|
+
* `--old-main`: Name of the old main LaTeX file which should be compared. Defaults to `--main`.
|
|
121
|
+
* `-d`, `--diff-name`: Name of the final diff file. '`.pdf`' will be appended if necessary. The log file of the last `pdflatex` call will be stored beside this file.
|
|
122
|
+
* `-w`, `--overwrite`: If not given `gitlatexdiff` refuses to overwite an existing diff file.
|
|
123
|
+
* `--num-rounds`: Number of calls to `pdflatex` when compiling the diff.
|
|
124
|
+
|
|
125
|
+
The following options are passed to `latexdiff` or `pdflatex` respectively. For technical reasons values have to be given without leading dashes. Dashes are prepended as required by the respective command.
|
|
126
|
+
|
|
127
|
+
* `-l`, `--latexdiff-options`: Arbitrary number of options passed to `latexdiff` call. Pass without any value to turn off the default.
|
|
128
|
+
* `-p`, `--pdflatex-options`: Arbitrary number of options passed to `pdflatex` call. Pass without any value to turn off the default.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
### `flattenlatex`
|
|
132
|
+
|
|
133
|
+
Python module to recusively resolve `\include` and `\input` commands in a LaTeX document. `gitlatexdiff` uses it on both versions of the input file.
|
|
134
|
+
|
|
135
|
+
Call `uv run flattenlatex --help` to see its options to set input and output file. The input file is mandatory, because included files are drawn from its directory. The output file is optional, if omitted, `stdout` will be used.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
Documentation is created with [MkDocs](https://www.mkdocs.org).
|
|
142
|
+
|
|
143
|
+
The following commands called from the base directory create the documentation:
|
|
144
|
+
|
|
145
|
+
* `uv run mkdocs serve` - Start the docs server.
|
|
146
|
+
* `uv run mkdocs build` - Build static documentation in subfolder `site/`
|
|
147
|
+
* `uv run mkdocs --help` - Print help message and exit.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
Create issues or a pull requests to point out bugs or improvements.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## A note on the name
|
|
157
|
+
|
|
158
|
+
This project is called *Git-LaTeX-Diff Original* or `gitlatexdiff-original`, because the project name *gitlatexdiff* is already in use on [PyPI](https://pypi.org) for a similar [package](https://pypi.org/project/gitlatexdiff/) that was independently developed. It is called *original* due to the fact that the first publication of this project on GitHub is older.
|
|
159
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Git-LaTeX-Diff Original
|
|
2
|
+
|
|
3
|
+
Make a rendered diff of two versions of a LaTeX document.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### License
|
|
7
|
+
|
|
8
|
+
See [License](LICENSE.md)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changelog
|
|
12
|
+
|
|
13
|
+
See [Changelog](CHANGELOG.md)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
[*latexdiff*](https://www.ctan.org/pkg/latexdiff) is a LaTeX tool to create a diff of two LaTeX documents, which shows deletions and additions as red strike-through text and additions as blue underlined text when compiled to PDF. However, *latexdiff* has some major limitations.
|
|
19
|
+
|
|
20
|
+
To overcome the limitations, this Python script extends *latexdiff* in several ways:
|
|
21
|
+
|
|
22
|
+
* It works with a Git repo such that it compares the current state or a given commit with an earlier commit
|
|
23
|
+
* It resolves `\include` and `\input` commands like LaTeX does
|
|
24
|
+
* It calls `pdflatex` to render the final PDF
|
|
25
|
+
|
|
26
|
+
In addition the `\include` and `\input` resolving itself can be called as standalone script.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Caveats
|
|
31
|
+
|
|
32
|
+
* While `\include` and `\input` are resolved from the respective git revisions, other includes like figures are resolved when compiling the diff. This is done in the new revision. So if the old version includes figures that are missing or renamed in the new revision they will be missing in the diff PDF as well.
|
|
33
|
+
* There is no diff of the bibliography and other generated parts.
|
|
34
|
+
* When using uncommitted changes as new version the rendering has to take place in the documents work directory. This may leave temporary files and have other unexpected side effects. However, if everything is committed or dedicated Git revisions are compared all processing is done in temporary directories that are cleaned up.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Prerequesites
|
|
39
|
+
|
|
40
|
+
* LaTeX must be installed including the tools
|
|
41
|
+
* `pdflatex`
|
|
42
|
+
* `latexdiff`
|
|
43
|
+
* Python3 is available with at least the version set in `pyproject.toml`
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
The easiest way is to apply the Python tool `uv`, which we describe here. Likely `poetry` will work as well.
|
|
50
|
+
|
|
51
|
+
Change to the directory where you cloned the gitlatexdiff project and call
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv sync
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
to install all dependencies and create a virtual environment.
|
|
58
|
+
|
|
59
|
+
To run the script call
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run gitlatexdiff <options>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To run the script to flatten a LeTeX file standalone call
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv run flattenlatex <options>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#### Tips
|
|
73
|
+
|
|
74
|
+
If the diff sources cannot be compiled check the log file for problems with `\DIF...` commands and see which original LaTeX command caused it. Then you may exclude that command from the diff with for example:
|
|
75
|
+
|
|
76
|
+
`-l 'append-textcmd=hint.*,todo' 'exclude-textcmd=title,.*section,chapter'`
|
|
77
|
+
|
|
78
|
+
Here, LaTeX commands `\title`, `\chapter`, and all ending in `section` are excluded, so diffs in these commands are not marked in the output.
|
|
79
|
+
|
|
80
|
+
Note, that in this case the `'append-textcmd=hint.*,todo'` is the default for option `-l`, which needs to be set explicitely if `-l` is given.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
### Options
|
|
84
|
+
|
|
85
|
+
Call `gitlatexdiff` with option `--help` to get the current list of command line options and their defaults if applicable.
|
|
86
|
+
|
|
87
|
+
#### Mandatory options
|
|
88
|
+
|
|
89
|
+
* `-m`, `--main`: Name of the main LaTeX file whose versions should be compared. It has to reside in the respective Git repository containing the versions to compare. May be given with path if `gitlatexdiff` is called from outside the LaTeX project directory.
|
|
90
|
+
|
|
91
|
+
#### Optional
|
|
92
|
+
|
|
93
|
+
All other command line options are optional.
|
|
94
|
+
|
|
95
|
+
Call `gitlatexdiff --help` to see the defaults of the following options.
|
|
96
|
+
|
|
97
|
+
* `-n`, `--new-rev`: Newer revision to compare with. If not given the current state of the work files is used, which will be the HEAD revision if all files are committed or else the work files.
|
|
98
|
+
* `-o`, `--old-rev`: Older revision to compare with. If not given either the revision before `--new-rev` is used or the HEAD revision if `--new-rev` is also not given and there are uncommitted changes.
|
|
99
|
+
* `--old-main`: Name of the old main LaTeX file which should be compared. Defaults to `--main`.
|
|
100
|
+
* `-d`, `--diff-name`: Name of the final diff file. '`.pdf`' will be appended if necessary. The log file of the last `pdflatex` call will be stored beside this file.
|
|
101
|
+
* `-w`, `--overwrite`: If not given `gitlatexdiff` refuses to overwite an existing diff file.
|
|
102
|
+
* `--num-rounds`: Number of calls to `pdflatex` when compiling the diff.
|
|
103
|
+
|
|
104
|
+
The following options are passed to `latexdiff` or `pdflatex` respectively. For technical reasons values have to be given without leading dashes. Dashes are prepended as required by the respective command.
|
|
105
|
+
|
|
106
|
+
* `-l`, `--latexdiff-options`: Arbitrary number of options passed to `latexdiff` call. Pass without any value to turn off the default.
|
|
107
|
+
* `-p`, `--pdflatex-options`: Arbitrary number of options passed to `pdflatex` call. Pass without any value to turn off the default.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
### `flattenlatex`
|
|
111
|
+
|
|
112
|
+
Python module to recusively resolve `\include` and `\input` commands in a LaTeX document. `gitlatexdiff` uses it on both versions of the input file.
|
|
113
|
+
|
|
114
|
+
Call `uv run flattenlatex --help` to see its options to set input and output file. The input file is mandatory, because included files are drawn from its directory. The output file is optional, if omitted, `stdout` will be used.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
Documentation is created with [MkDocs](https://www.mkdocs.org).
|
|
121
|
+
|
|
122
|
+
The following commands called from the base directory create the documentation:
|
|
123
|
+
|
|
124
|
+
* `uv run mkdocs serve` - Start the docs server.
|
|
125
|
+
* `uv run mkdocs build` - Build static documentation in subfolder `site/`
|
|
126
|
+
* `uv run mkdocs --help` - Print help message and exit.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## Contributing
|
|
131
|
+
|
|
132
|
+
Create issues or a pull requests to point out bugs or improvements.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## A note on the name
|
|
136
|
+
|
|
137
|
+
This project is called *Git-LaTeX-Diff Original* or `gitlatexdiff-original`, because the project name *gitlatexdiff* is already in use on [PyPI](https://pypi.org) for a similar [package](https://pypi.org/project/gitlatexdiff/) that was independently developed. It is called *original* due to the fact that the first publication of this project on GitHub is older.
|
|
138
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Copyright 2019,2026 Björn Hendriks
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
[project]
|
|
19
|
+
name = "gitlatexdiff-original"
|
|
20
|
+
version = "0.2.0"
|
|
21
|
+
description = "Make diff of two versions of a LaTeX document in a Git repo"
|
|
22
|
+
readme = "README.md"
|
|
23
|
+
license = "Apache-2.0"
|
|
24
|
+
license-files = [ "LICENSE" ]
|
|
25
|
+
keywords = [ "git", "latex", "diff", "tool" ]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
29
|
+
]
|
|
30
|
+
authors = [
|
|
31
|
+
{name = "bjhend", email = "developer@bjhend.de"},
|
|
32
|
+
]
|
|
33
|
+
maintainers = [
|
|
34
|
+
{name = "bjhend", email = "developer@bjhend.de"},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
requires-python = ">=3.12"
|
|
38
|
+
dependencies = [
|
|
39
|
+
"beartype",
|
|
40
|
+
"mkdocs",
|
|
41
|
+
"mkdocstrings[python]",
|
|
42
|
+
"pymdown-extensions",
|
|
43
|
+
"icecream",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Repository = "https://github.com/bjhend/gitlatexdiff"
|
|
49
|
+
Issues = "https://github.com/bjhend/gitlatexdiff/issues"
|
|
50
|
+
Changelog = "https://github.com/bjhend/gitlatexdiff/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
gitlatexdiff = "gitlatexdiff:main"
|
|
55
|
+
flattenlatex = "gitlatexdiff:flattenCommand"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
[build-system]
|
|
59
|
+
requires = ["setuptools>=61.0"]
|
|
60
|
+
build-backend = "setuptools.build_meta"
|
|
61
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Copyright 2019,2026 Björn Hendriks
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Dynamic type checking with beartype
|
|
19
|
+
from beartype.claw import beartype_this_package
|
|
20
|
+
beartype_this_package()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from .make_diff import main
|
|
24
|
+
from .flatten_latex import flattenCommand
|
|
25
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Recursively replace LaTeX input and include commands by the respective files
|
|
5
|
+
#
|
|
6
|
+
# Can be used either as module providing the parseFile function or directly
|
|
7
|
+
# called with LaTeX code provided on stdin and the result written to stdout.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Copyright 2019,2026 Björn Hendriks
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import typing
|
|
29
|
+
import argparse
|
|
30
|
+
import pathlib as pl
|
|
31
|
+
import contextlib
|
|
32
|
+
from icecream import ic
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Config():
|
|
37
|
+
"""Configuration values"""
|
|
38
|
+
|
|
39
|
+
texExtension = '.tex'
|
|
40
|
+
|
|
41
|
+
# Regular expression template to find LaTeX commands which are not commented out
|
|
42
|
+
# the first part only allows '%' with an odd number of leading backslashes
|
|
43
|
+
_commandPattern = r"^(?P<before>(?:[^%\\]|\\.)*)\\{cmd}{{(?P<filename>.*?)}}(?P<after>.*)"
|
|
44
|
+
inputRe = re.compile(_commandPattern.format(cmd='input'))
|
|
45
|
+
includeRe = re.compile(_commandPattern.format(cmd='include'))
|
|
46
|
+
includeOnlyRe = re.compile(_commandPattern.format(cmd='includeonly'))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _flattenRecursion(inFile:typing.TextIO, outFile:typing.TextIO,
|
|
50
|
+
includeOnly:list[str]|None=None, isResolveInclude:bool=True) -> None:
|
|
51
|
+
"""Copy inFile to outFile recursively inserting inputs and includes
|
|
52
|
+
|
|
53
|
+
According to LaTeX `\\input` is always inserted but `\\include` is more special.
|
|
54
|
+
`\\include` cannot be nested and if `\\includeonly` is given in the preamble
|
|
55
|
+
includes are limited to those files. In addition `\\include` is surrounded by
|
|
56
|
+
`\\clearpage`.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
inFile: readable text file object to parse
|
|
60
|
+
outFile: writable text file object to write result into
|
|
61
|
+
includeOnly: list of filenames to include found in `\\includeonly` command
|
|
62
|
+
isResolveInclude: True if `\\include` should be resolved to avoid nested inclusion
|
|
63
|
+
"""
|
|
64
|
+
config = _Config()
|
|
65
|
+
|
|
66
|
+
def getFilename(match:re.Match) -> str:
|
|
67
|
+
"""Get filename argument from command match"""
|
|
68
|
+
return match.group('filename').strip()
|
|
69
|
+
|
|
70
|
+
def insertFile(match:re.Match, isInclude:bool) -> None:
|
|
71
|
+
"""Open filename in match and insert its content"""
|
|
72
|
+
|
|
73
|
+
if isInclude:
|
|
74
|
+
label = 'include'
|
|
75
|
+
surround = "\\clearpage % inserted due to resolved include\n"
|
|
76
|
+
newIsResolveInclude = False
|
|
77
|
+
else:
|
|
78
|
+
label = 'input'
|
|
79
|
+
surround = ''
|
|
80
|
+
newIsResolveInclude = isResolveInclude
|
|
81
|
+
|
|
82
|
+
fileName = getFilename(match)
|
|
83
|
+
if not fileName.endswith(config.texExtension):
|
|
84
|
+
fileName += config.texExtension
|
|
85
|
+
before = match.group('before')
|
|
86
|
+
after = match.group('after')
|
|
87
|
+
|
|
88
|
+
outFile.write(before)
|
|
89
|
+
outFile.write(f"% ========= begin {label} of {fileName} ==========\n")
|
|
90
|
+
outFile.write(surround)
|
|
91
|
+
with open(fileName) as file:
|
|
92
|
+
_flattenRecursion(file, outFile, includeOnly=includeOnly,
|
|
93
|
+
isResolveInclude=newIsResolveInclude)
|
|
94
|
+
outFile.write(surround)
|
|
95
|
+
outFile.write(f"% ========= end {label} of {fileName} ==========\n")
|
|
96
|
+
outFile.write(after)
|
|
97
|
+
|
|
98
|
+
for line in inFile:
|
|
99
|
+
# Handle \includeonly
|
|
100
|
+
matchIncludeOnly = config.includeOnlyRe.search(line)
|
|
101
|
+
if matchIncludeOnly:
|
|
102
|
+
filenames = getFilename(matchIncludeOnly).split(sep=',')
|
|
103
|
+
includeOnly = [ f.strip() for f in filenames ]
|
|
104
|
+
|
|
105
|
+
# Handle \input
|
|
106
|
+
matchInput = config.inputRe.search(line)
|
|
107
|
+
if matchInput:
|
|
108
|
+
insertFile(match=matchInput, isInclude=False)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Handle \include
|
|
112
|
+
if isResolveInclude:
|
|
113
|
+
matchInclude = config.includeRe.search(line)
|
|
114
|
+
if matchInclude:
|
|
115
|
+
if (not includeOnly) or (getFilename(matchInclude) in includeOnly):
|
|
116
|
+
insertFile(match=matchInclude, isInclude=True)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Copy line
|
|
120
|
+
outFile.write(line)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def flatten(inFile:typing.TextIO, outFile:typing.TextIO) -> None:
|
|
124
|
+
"""Start recursively resolving inputs/includes
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
inFile: An open read text file object to read input LaTeX code from
|
|
128
|
+
OutFile: An open write text file object to write flattened LaTeX code to
|
|
129
|
+
"""
|
|
130
|
+
_flattenRecursion(inFile, outFile)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def flattenFiles(inPath:pl.Path, outPath:pl.Path|None=None) -> None:
|
|
134
|
+
"""Open files and call flatten() with them
|
|
135
|
+
|
|
136
|
+
Both files can be given as pathlib.Path or string.
|
|
137
|
+
|
|
138
|
+
We require inPath instead of defaulting to stdin, because we need
|
|
139
|
+
its directory to resolve relative input/include paths in the LaTeX code
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
inPath: path of input file
|
|
143
|
+
outPath: optional name of output file, if omitted stdout is used instead
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
OSError: if a file cannot be opened.
|
|
147
|
+
"""
|
|
148
|
+
cwd = pl.Path.cwd()
|
|
149
|
+
try:
|
|
150
|
+
inPath = pl.Path(inPath)
|
|
151
|
+
assert inPath.is_file()
|
|
152
|
+
|
|
153
|
+
inPathAbs = inPath.absolute()
|
|
154
|
+
if outPath:
|
|
155
|
+
outPath = pl.Path(outPath)
|
|
156
|
+
output:contextlib.AbstractContextManager[typing.TextIO] = contextlib.closing(outPath.absolute().open('w'))
|
|
157
|
+
else:
|
|
158
|
+
output = contextlib.nullcontext(sys.stdout)
|
|
159
|
+
|
|
160
|
+
# Change dir to resolve relative inputs/includes
|
|
161
|
+
os.chdir(inPathAbs.parent)
|
|
162
|
+
|
|
163
|
+
with inPathAbs.open() as inFile:
|
|
164
|
+
with output as outFile:
|
|
165
|
+
flatten(inFile, outFile)
|
|
166
|
+
finally:
|
|
167
|
+
os.chdir(cwd)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def flattenCommand() -> None:
|
|
171
|
+
"""Call flatten from command line
|
|
172
|
+
|
|
173
|
+
Parses command line for input and output and calls flattenFiles with them.
|
|
174
|
+
|
|
175
|
+
If output exists an additional command line flag is required to overwrite it.
|
|
176
|
+
"""
|
|
177
|
+
parser = argparse.ArgumentParser(description="Flatten a LaTeX file to a single file with all input/include resolved")
|
|
178
|
+
|
|
179
|
+
# See flattenFiles documentation, why --input is required
|
|
180
|
+
parser.add_argument('-i', '--input', required=True, type=pl.Path, help="Main LaTeX file to flatten")
|
|
181
|
+
parser.add_argument('-o', '--output', type=pl.Path, help="Output file, default is stdout")
|
|
182
|
+
parser.add_argument('-w', '--overwrite', action='store_true', help="Silently overwrite existing diff? (default: %(default)s)")
|
|
183
|
+
args = parser.parse_args()
|
|
184
|
+
|
|
185
|
+
if args.output and (not args.overwrite) and args.output.exists():
|
|
186
|
+
print(f"Output {args.output} exists. Delete it or set option --overwrite (-w) to overwrite it.")
|
|
187
|
+
exit(1)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
flattenFiles(args.input, args.output)
|
|
191
|
+
except OSError as ex:
|
|
192
|
+
print(f"Cannot open input or output file: {ex}")
|
|
193
|
+
exit(1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
flattenCommand()
|
|
198
|
+
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Make a diff of a LaTeX file to another Git revision with latexdiff
|
|
5
|
+
#
|
|
6
|
+
# Call it with option --help for more info
|
|
7
|
+
|
|
8
|
+
# Copyright 2019,2026 Björn Hendriks
|
|
9
|
+
#
|
|
10
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
# you may not use this file except in compliance with the License.
|
|
12
|
+
# You may obtain a copy of the License at
|
|
13
|
+
#
|
|
14
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
15
|
+
#
|
|
16
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
# See the License for the specific language governing permissions and
|
|
20
|
+
# limitations under the License.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import argparse
|
|
26
|
+
import subprocess
|
|
27
|
+
import tempfile
|
|
28
|
+
import pathlib as pl
|
|
29
|
+
import contextlib
|
|
30
|
+
import typing
|
|
31
|
+
from collections.abc import Generator
|
|
32
|
+
import shutil
|
|
33
|
+
from icecream import ic
|
|
34
|
+
from . import flatten_latex
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
latexExtension = '.tex'
|
|
38
|
+
pdfExtension = '.pdf'
|
|
39
|
+
logExtension = '.log'
|
|
40
|
+
messagePrefix = "------ "
|
|
41
|
+
|
|
42
|
+
# Command line option defaults
|
|
43
|
+
defaultNumRounds = 3
|
|
44
|
+
defaultDiffName = 'diff'
|
|
45
|
+
defaultLatexdiffOptions = ['append-textcmd=hint.*,todo']
|
|
46
|
+
defaultPdflatexOptions = ['interaction=batchmode']
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def callCommand(args:list[str], cwd:pl.Path|None=None) -> str:
|
|
50
|
+
"""Call args as shell command and return its stdout (NOT stderr)
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
cwd: optional working directory
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
subprocess.CalledProcessError: if the command returns with non-zero
|
|
57
|
+
exit code
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
stdout of the command
|
|
61
|
+
"""
|
|
62
|
+
result = subprocess.run(args, cwd=cwd, stdout=subprocess.PIPE, check=True)
|
|
63
|
+
return result.stdout.decode().strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Configuration():
|
|
67
|
+
"""Configuration values either from parsing command line or hard coded"""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
"""Get and preprocess command line arguments"""
|
|
71
|
+
args = self._parseArgs()
|
|
72
|
+
|
|
73
|
+
# Options given to latexdiff
|
|
74
|
+
self.latexdiffOptions = self._prependPrefix('--', args.latexdiff_options)
|
|
75
|
+
# Options given to pdflatex
|
|
76
|
+
self.pdflatexOptions = self._prependPrefix('-', args.pdflatex_options)
|
|
77
|
+
# Number of successive calls of pdflatex to achieve the final result
|
|
78
|
+
self.numTexRounds = args.num_rounds
|
|
79
|
+
if self.numTexRounds < 1:
|
|
80
|
+
print(f"--num-rounds set to {self.numTexRounds}, but must be at least 1")
|
|
81
|
+
exit(1)
|
|
82
|
+
|
|
83
|
+
self.mainFileAbs = args.main.resolve().with_suffix(latexExtension)
|
|
84
|
+
self.oldMainFileAbs = args.old_main.resolve().with_suffix(latexExtension) if args.old_main else self.mainFileAbs
|
|
85
|
+
self.diffNameAbs = args.diff_name.resolve().with_suffix(pdfExtension)
|
|
86
|
+
self.logNameAbs = args.diff_name.resolve().with_suffix(logExtension)
|
|
87
|
+
|
|
88
|
+
# Bail out if diffNameAbs exists and no --overwrite given
|
|
89
|
+
if (not args.overwrite) and self.diffNameAbs.exists():
|
|
90
|
+
print(f"Destination file {self.diffNameAbs} exists. Delete it or set --overwrite to overwrite it.")
|
|
91
|
+
exit(1)
|
|
92
|
+
|
|
93
|
+
self.newRevision = args.new_rev
|
|
94
|
+
self.oldRevision = args.old_rev
|
|
95
|
+
|
|
96
|
+
def _prependPrefix(self, prefix:str, options:list[str]) -> list[str]:
|
|
97
|
+
"""Prepend prefix to all elements of options tuple
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
prefix: string to prepend to all elements of options
|
|
101
|
+
options: iterable of strings to prepend prefix to
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
options with prefix prepended
|
|
105
|
+
"""
|
|
106
|
+
return [ prefix + opt for opt in options ]
|
|
107
|
+
|
|
108
|
+
def _parseArgs(self) -> argparse.Namespace:
|
|
109
|
+
"""Parse command line arguments
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
result of argparse.ArgumentParser.parse_args()
|
|
113
|
+
"""
|
|
114
|
+
parser = argparse.ArgumentParser(description='Make a LaTeX diff for two Git revisions of a LaTeX project')
|
|
115
|
+
|
|
116
|
+
parser.add_argument('-m', '--main', type=pl.Path, required=True, help='Main LaTeX file (required)')
|
|
117
|
+
parser.add_argument('-n', '--new-rev', help='Ref to new revision for diff (default: current state of the repo)')
|
|
118
|
+
parser.add_argument('-o', '--old-rev', help='Ref to old revision for diff (default depends on --new-rev: If --new-rev is given'
|
|
119
|
+
' one revision before --new-rev. If --new-rev is omitted but everything is committed'
|
|
120
|
+
' then one before HEAD. Else HEAD itself.)')
|
|
121
|
+
parser.add_argument('--old-main', type=pl.Path, help='Main LaTeX file of old revision (defaults to --main)')
|
|
122
|
+
parser.add_argument('-d', '--diff-name', type=pl.Path, default=defaultDiffName, help='Name for final diff file (default: %(default)s)')
|
|
123
|
+
parser.add_argument('-w', '--overwrite', action='store_true', help='Silently overwrite existing diff (default: %(default)s)')
|
|
124
|
+
parser.add_argument('--num-rounds', type=int, default=defaultNumRounds, help='Number of pdflatexcalls to compile the diff (default: %(default)s)')
|
|
125
|
+
parser.add_argument('-l', '--latexdiff-options', nargs='*', default=defaultLatexdiffOptions,
|
|
126
|
+
help='Options passed to latexdiff without leading dashes (default: %(default)s)')
|
|
127
|
+
parser.add_argument('-p', '--pdflatex-options', nargs='*', default=defaultPdflatexOptions,
|
|
128
|
+
help='Options passed to pdflatex without leading dashes (default: %(default)s)')
|
|
129
|
+
|
|
130
|
+
return parser.parse_args()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class GitRepo():
|
|
134
|
+
"""Wrapper for all Git commands
|
|
135
|
+
|
|
136
|
+
We do not apply GitPython or other third party packages to be as portable
|
|
137
|
+
as possible. Instead we call the Git commands directly.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, config:Configuration):
|
|
141
|
+
"""Init GitRepo with given config
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
config: configuration with main file path
|
|
145
|
+
"""
|
|
146
|
+
self.repoDir = pl.Path(self._callGit(['rev-parse', '--show-toplevel'], config.mainFileAbs.parent))
|
|
147
|
+
|
|
148
|
+
def getSha1(self, committish:str) -> str:
|
|
149
|
+
"""Return SHA1 of committish
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
committish: any string that Git recognizes as a commit reference: HEAD,
|
|
153
|
+
branch, tag, or their parents
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
SHA1 of the referenced commit or `None` if it cannot be resolved
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
return self._callGit(['rev-parse', committish])
|
|
161
|
+
except subprocess.CalledProcessError:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def isDirty(self) -> bool:
|
|
165
|
+
"""Check if uncommitted changes or new non-ignored files are present
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
`True` if there are uncommitted files
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# Returns with non-zero exitcode if a committed file is altered
|
|
172
|
+
self._callGit(['diff-index', '--quiet', 'HEAD', '--'])
|
|
173
|
+
except subprocess.CalledProcessError:
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
# Returns list of non-ignored untracked files
|
|
177
|
+
untrackedFiles = self._callGit(['ls-files', '--exclude-standard', '--others'])
|
|
178
|
+
return bool(untrackedFiles)
|
|
179
|
+
|
|
180
|
+
@contextlib.contextmanager
|
|
181
|
+
def worktree(self, sha1:str|None=None) -> Generator[pl.Path]:
|
|
182
|
+
"""Check out sha1 in a temporary worktree and finally cleanup or return repo dir
|
|
183
|
+
|
|
184
|
+
This is a contextmanager, so call it in a `with` statement.
|
|
185
|
+
|
|
186
|
+
If `sha1` is given check it out in a temporary worktree and remove the
|
|
187
|
+
worktree on exit of the context. If `sha1` is `None` return the repo dir
|
|
188
|
+
itself.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
sha1: SHA1 of the commit to check out in the worktree, if `None` return
|
|
192
|
+
the repo dir itself
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
path to the work files
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if not sha1:
|
|
199
|
+
yield self.repoDir
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
with tempfile.TemporaryDirectory(prefix='gitlatexdiff_worktree_') as workDir:
|
|
203
|
+
workDirPath = pl.Path(workDir)
|
|
204
|
+
self._callGit(['worktree', 'add', '--force', str(workDirPath), sha1])
|
|
205
|
+
try:
|
|
206
|
+
yield workDirPath
|
|
207
|
+
finally:
|
|
208
|
+
self._callGit(['worktree', 'remove', str(workDirPath)])
|
|
209
|
+
|
|
210
|
+
def _callGit(self, args:list[str], workingDir:pl.Path|None=None) -> str:
|
|
211
|
+
"""Call a Git command in the repo or workingDir if given
|
|
212
|
+
|
|
213
|
+
Prepends `git` to the given args and calls callCommand() with them.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
args: command line arguments for the Git command (without `git` itself)
|
|
217
|
+
workingDir: optional dir to execute the command in, if ommitted use
|
|
218
|
+
the repo dir
|
|
219
|
+
|
|
220
|
+
Return:
|
|
221
|
+
stdout of the command
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
see callCommand()
|
|
225
|
+
"""
|
|
226
|
+
if workingDir is None:
|
|
227
|
+
workingDir = self.repoDir
|
|
228
|
+
return callCommand(['git'] + args, cwd=workingDir)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Diff():
|
|
233
|
+
"""Class to create the diff"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, config:Configuration, gitRepo:GitRepo):
|
|
236
|
+
"""Init
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
config: configuration
|
|
240
|
+
gitRepo: Git repo
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
self.config = config
|
|
244
|
+
self.gitRepo = gitRepo
|
|
245
|
+
self.mainFileRelative = self.config.mainFileAbs.relative_to(self.gitRepo.repoDir)
|
|
246
|
+
self.oldMainFileRelative = self.config.oldMainFileAbs.relative_to(self.gitRepo.repoDir)
|
|
247
|
+
|
|
248
|
+
# Determine new sha1
|
|
249
|
+
if config.newRevision:
|
|
250
|
+
self.newSha1:str|None = self.gitRepo.getSha1(config.newRevision)
|
|
251
|
+
else:
|
|
252
|
+
if self.gitRepo.isDirty():
|
|
253
|
+
self.newSha1 = None
|
|
254
|
+
else:
|
|
255
|
+
self.newSha1 = self.gitRepo.getSha1('HEAD')
|
|
256
|
+
|
|
257
|
+
# Determine old sha1
|
|
258
|
+
if config.oldRevision:
|
|
259
|
+
self.oldSha1 = self.gitRepo.getSha1(config.oldRevision)
|
|
260
|
+
else:
|
|
261
|
+
if self.newSha1:
|
|
262
|
+
# Use one revision before new sha1
|
|
263
|
+
self.oldSha1 = self.gitRepo.getSha1(self.newSha1 + '~')
|
|
264
|
+
else:
|
|
265
|
+
# Compare work files with HEAD revision
|
|
266
|
+
self.oldSha1 = self.gitRepo.getSha1('HEAD')
|
|
267
|
+
|
|
268
|
+
@contextlib.contextmanager
|
|
269
|
+
def _flatFile(self, sha1:str|None, mainFileRelative:pl.Path) -> Generator[pl.Path]:
|
|
270
|
+
"""Flatten mainFileRelative in the given sha1 revision and return its path
|
|
271
|
+
|
|
272
|
+
This method is a contextmanager. So it needs to be called in a
|
|
273
|
+
with-statement. On leaving the context the returned file and the worktree
|
|
274
|
+
will be removed.
|
|
275
|
+
|
|
276
|
+
Flatten resolves all include/input commands.
|
|
277
|
+
|
|
278
|
+
If sha1 is not None flattening is done in an exclusive worktree to avoid
|
|
279
|
+
interfering with the repo.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
sha1: revision to check out, if None use the repo itself
|
|
283
|
+
mainFileRelative: relative path to the main file in the repo
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
name of the temporary file
|
|
287
|
+
"""
|
|
288
|
+
version = f"version {sha1}" if sha1 else "current version"
|
|
289
|
+
print(f"{messagePrefix}Flattening {mainFileRelative} in {version}")
|
|
290
|
+
with self.gitRepo.worktree(sha1) as workDir:
|
|
291
|
+
mainFileDir = workDir / mainFileRelative.parent
|
|
292
|
+
os.chdir(mainFileDir)
|
|
293
|
+
with tempfile.NamedTemporaryFile(mode='w',
|
|
294
|
+
prefix='flattened_',
|
|
295
|
+
suffix=latexExtension,
|
|
296
|
+
dir=mainFileDir,
|
|
297
|
+
delete_on_close=False) as texFile:
|
|
298
|
+
with (workDir / mainFileRelative).open() as mainFile:
|
|
299
|
+
flatten_latex.flatten(mainFile, typing.cast(typing.TextIO, texFile.file))
|
|
300
|
+
texFile.close()
|
|
301
|
+
yield pl.Path(texFile.name)
|
|
302
|
+
|
|
303
|
+
def makeDiff(self) -> None:
|
|
304
|
+
"""Create the diff PDF file in the directory this script was called from"""
|
|
305
|
+
|
|
306
|
+
# Make LaTeX diff
|
|
307
|
+
with (self._flatFile(self.oldSha1, self.oldMainFileRelative) as oldFlatInput,
|
|
308
|
+
self._flatFile(self.newSha1, self.mainFileRelative) as newFlatInput):
|
|
309
|
+
print(f"{messagePrefix}Create diff")
|
|
310
|
+
diffTex = callCommand(['latexdiff'] + self.config.latexdiffOptions
|
|
311
|
+
+ [str(oldFlatInput), str(newFlatInput)])
|
|
312
|
+
|
|
313
|
+
# Compile diff in a worktree and move it to its configure final path.
|
|
314
|
+
# Note that workDir is the repo itself if self.newSha1 is None.
|
|
315
|
+
with self.gitRepo.worktree(self.newSha1) as workDir:
|
|
316
|
+
mainFileDir = workDir / self.mainFileRelative.parent
|
|
317
|
+
|
|
318
|
+
# TeX commands need to be executed in their repo to have correct access
|
|
319
|
+
# to all temporary TeX files
|
|
320
|
+
os.chdir(mainFileDir)
|
|
321
|
+
|
|
322
|
+
with tempfile.NamedTemporaryFile(mode='w',
|
|
323
|
+
prefix='diff_',
|
|
324
|
+
suffix=latexExtension,
|
|
325
|
+
dir=mainFileDir,
|
|
326
|
+
delete_on_close=False) as diffTexFile:
|
|
327
|
+
diffTexFile.write(diffTex)
|
|
328
|
+
diffTexFile.close()
|
|
329
|
+
diffTexFilePath = pl.Path(diffTexFile.name)
|
|
330
|
+
|
|
331
|
+
# Call pdflatex sufficiently often on diff
|
|
332
|
+
for i in range(self.config.numTexRounds):
|
|
333
|
+
print(f"{messagePrefix}Compiling diff round {i+1}")
|
|
334
|
+
try:
|
|
335
|
+
callCommand(['pdflatex'] + self.config.pdflatexOptions + [str(diffTexFilePath)], cwd=mainFileDir)
|
|
336
|
+
except subprocess.CalledProcessError as ex:
|
|
337
|
+
# Sometimes a pdfltex call returns an error but still works
|
|
338
|
+
print(f"Warning: pdflatex returned an error, which we ignore: {ex}")
|
|
339
|
+
print(f"See pdflatex log for possible causes: {self.config.logNameAbs}")
|
|
340
|
+
|
|
341
|
+
# Move resulting PDF and log to initial dir
|
|
342
|
+
diffPdfPathname = diffTexFilePath.with_suffix(pdfExtension)
|
|
343
|
+
diffLogPathname = diffTexFilePath.with_suffix(logExtension)
|
|
344
|
+
shutil.move(diffLogPathname, self.config.logNameAbs)
|
|
345
|
+
try:
|
|
346
|
+
shutil.move(diffPdfPathname, self.config.diffNameAbs)
|
|
347
|
+
except FileNotFoundError:
|
|
348
|
+
print(f"No diff created. See log files for possible cause: {self.config.logNameAbs}")
|
|
349
|
+
print("Hint: It may help exclude LaTeX commands with for example \"-l 'exclude-textcmd=title,.*section,chapter'\"")
|
|
350
|
+
|
|
351
|
+
# Remove all temporary pdflatex files
|
|
352
|
+
# This is only relevant if a diff with current dirty work files was made,
|
|
353
|
+
# so it was compiled in the repo itself instead of a temp worktree.
|
|
354
|
+
for path in diffTexFilePath.parent.glob(f'{diffTexFilePath.stem}*'):
|
|
355
|
+
path.unlink()
|
|
356
|
+
|
|
357
|
+
print(f"{messagePrefix}Successfully created diff {self.config.diffNameAbs}")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def main() -> None:
|
|
361
|
+
"""Entry point"""
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
config = Configuration()
|
|
365
|
+
gitRepo = GitRepo(config)
|
|
366
|
+
diff = Diff(config, gitRepo)
|
|
367
|
+
diff.makeDiff()
|
|
368
|
+
except Exception as ex:
|
|
369
|
+
print(f"An error occurred: {ex}")
|
|
370
|
+
exit(1)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
main()
|
|
375
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitlatexdiff-original
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Make diff of two versions of a LaTeX document in a Git repo
|
|
5
|
+
Author-email: bjhend <developer@bjhend.de>
|
|
6
|
+
Maintainer-email: bjhend <developer@bjhend.de>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Project-URL: Repository, https://github.com/bjhend/gitlatexdiff
|
|
9
|
+
Project-URL: Issues, https://github.com/bjhend/gitlatexdiff/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/bjhend/gitlatexdiff/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: git,latex,diff,tool
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: beartype
|
|
17
|
+
Requires-Dist: mkdocs
|
|
18
|
+
Requires-Dist: mkdocstrings[python]
|
|
19
|
+
Requires-Dist: pymdown-extensions
|
|
20
|
+
Requires-Dist: icecream
|
|
21
|
+
|
|
22
|
+
# Git-LaTeX-Diff Original
|
|
23
|
+
|
|
24
|
+
Make a rendered diff of two versions of a LaTeX document.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### License
|
|
28
|
+
|
|
29
|
+
See [License](LICENSE.md)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Changelog
|
|
33
|
+
|
|
34
|
+
See [Changelog](CHANGELOG.md)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Purpose
|
|
38
|
+
|
|
39
|
+
[*latexdiff*](https://www.ctan.org/pkg/latexdiff) is a LaTeX tool to create a diff of two LaTeX documents, which shows deletions and additions as red strike-through text and additions as blue underlined text when compiled to PDF. However, *latexdiff* has some major limitations.
|
|
40
|
+
|
|
41
|
+
To overcome the limitations, this Python script extends *latexdiff* in several ways:
|
|
42
|
+
|
|
43
|
+
* It works with a Git repo such that it compares the current state or a given commit with an earlier commit
|
|
44
|
+
* It resolves `\include` and `\input` commands like LaTeX does
|
|
45
|
+
* It calls `pdflatex` to render the final PDF
|
|
46
|
+
|
|
47
|
+
In addition the `\include` and `\input` resolving itself can be called as standalone script.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Caveats
|
|
52
|
+
|
|
53
|
+
* While `\include` and `\input` are resolved from the respective git revisions, other includes like figures are resolved when compiling the diff. This is done in the new revision. So if the old version includes figures that are missing or renamed in the new revision they will be missing in the diff PDF as well.
|
|
54
|
+
* There is no diff of the bibliography and other generated parts.
|
|
55
|
+
* When using uncommitted changes as new version the rendering has to take place in the documents work directory. This may leave temporary files and have other unexpected side effects. However, if everything is committed or dedicated Git revisions are compared all processing is done in temporary directories that are cleaned up.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
### Prerequesites
|
|
60
|
+
|
|
61
|
+
* LaTeX must be installed including the tools
|
|
62
|
+
* `pdflatex`
|
|
63
|
+
* `latexdiff`
|
|
64
|
+
* Python3 is available with at least the version set in `pyproject.toml`
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
The easiest way is to apply the Python tool `uv`, which we describe here. Likely `poetry` will work as well.
|
|
71
|
+
|
|
72
|
+
Change to the directory where you cloned the gitlatexdiff project and call
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
to install all dependencies and create a virtual environment.
|
|
79
|
+
|
|
80
|
+
To run the script call
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run gitlatexdiff <options>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To run the script to flatten a LeTeX file standalone call
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run flattenlatex <options>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#### Tips
|
|
94
|
+
|
|
95
|
+
If the diff sources cannot be compiled check the log file for problems with `\DIF...` commands and see which original LaTeX command caused it. Then you may exclude that command from the diff with for example:
|
|
96
|
+
|
|
97
|
+
`-l 'append-textcmd=hint.*,todo' 'exclude-textcmd=title,.*section,chapter'`
|
|
98
|
+
|
|
99
|
+
Here, LaTeX commands `\title`, `\chapter`, and all ending in `section` are excluded, so diffs in these commands are not marked in the output.
|
|
100
|
+
|
|
101
|
+
Note, that in this case the `'append-textcmd=hint.*,todo'` is the default for option `-l`, which needs to be set explicitely if `-l` is given.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### Options
|
|
105
|
+
|
|
106
|
+
Call `gitlatexdiff` with option `--help` to get the current list of command line options and their defaults if applicable.
|
|
107
|
+
|
|
108
|
+
#### Mandatory options
|
|
109
|
+
|
|
110
|
+
* `-m`, `--main`: Name of the main LaTeX file whose versions should be compared. It has to reside in the respective Git repository containing the versions to compare. May be given with path if `gitlatexdiff` is called from outside the LaTeX project directory.
|
|
111
|
+
|
|
112
|
+
#### Optional
|
|
113
|
+
|
|
114
|
+
All other command line options are optional.
|
|
115
|
+
|
|
116
|
+
Call `gitlatexdiff --help` to see the defaults of the following options.
|
|
117
|
+
|
|
118
|
+
* `-n`, `--new-rev`: Newer revision to compare with. If not given the current state of the work files is used, which will be the HEAD revision if all files are committed or else the work files.
|
|
119
|
+
* `-o`, `--old-rev`: Older revision to compare with. If not given either the revision before `--new-rev` is used or the HEAD revision if `--new-rev` is also not given and there are uncommitted changes.
|
|
120
|
+
* `--old-main`: Name of the old main LaTeX file which should be compared. Defaults to `--main`.
|
|
121
|
+
* `-d`, `--diff-name`: Name of the final diff file. '`.pdf`' will be appended if necessary. The log file of the last `pdflatex` call will be stored beside this file.
|
|
122
|
+
* `-w`, `--overwrite`: If not given `gitlatexdiff` refuses to overwite an existing diff file.
|
|
123
|
+
* `--num-rounds`: Number of calls to `pdflatex` when compiling the diff.
|
|
124
|
+
|
|
125
|
+
The following options are passed to `latexdiff` or `pdflatex` respectively. For technical reasons values have to be given without leading dashes. Dashes are prepended as required by the respective command.
|
|
126
|
+
|
|
127
|
+
* `-l`, `--latexdiff-options`: Arbitrary number of options passed to `latexdiff` call. Pass without any value to turn off the default.
|
|
128
|
+
* `-p`, `--pdflatex-options`: Arbitrary number of options passed to `pdflatex` call. Pass without any value to turn off the default.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
### `flattenlatex`
|
|
132
|
+
|
|
133
|
+
Python module to recusively resolve `\include` and `\input` commands in a LaTeX document. `gitlatexdiff` uses it on both versions of the input file.
|
|
134
|
+
|
|
135
|
+
Call `uv run flattenlatex --help` to see its options to set input and output file. The input file is mandatory, because included files are drawn from its directory. The output file is optional, if omitted, `stdout` will be used.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
Documentation is created with [MkDocs](https://www.mkdocs.org).
|
|
142
|
+
|
|
143
|
+
The following commands called from the base directory create the documentation:
|
|
144
|
+
|
|
145
|
+
* `uv run mkdocs serve` - Start the docs server.
|
|
146
|
+
* `uv run mkdocs build` - Build static documentation in subfolder `site/`
|
|
147
|
+
* `uv run mkdocs --help` - Print help message and exit.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
Create issues or a pull requests to point out bugs or improvements.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## A note on the name
|
|
157
|
+
|
|
158
|
+
This project is called *Git-LaTeX-Diff Original* or `gitlatexdiff-original`, because the project name *gitlatexdiff* is already in use on [PyPI](https://pypi.org) for a similar [package](https://pypi.org/project/gitlatexdiff/) that was independently developed. It is called *original* due to the fact that the first publication of this project on GitHub is older.
|
|
159
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gitlatexdiff/__init__.py
|
|
4
|
+
src/gitlatexdiff/flatten_latex.py
|
|
5
|
+
src/gitlatexdiff/make_diff.py
|
|
6
|
+
src/gitlatexdiff_original.egg-info/PKG-INFO
|
|
7
|
+
src/gitlatexdiff_original.egg-info/SOURCES.txt
|
|
8
|
+
src/gitlatexdiff_original.egg-info/dependency_links.txt
|
|
9
|
+
src/gitlatexdiff_original.egg-info/entry_points.txt
|
|
10
|
+
src/gitlatexdiff_original.egg-info/requires.txt
|
|
11
|
+
src/gitlatexdiff_original.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitlatexdiff
|