bibsync 0.3.1__tar.gz → 0.3.3__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.
- {bibsync-0.3.1 → bibsync-0.3.3}/CITATION.cff +2 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/Cargo.lock +13 -13
- {bibsync-0.3.1 → bibsync-0.3.3}/Cargo.toml +1 -1
- {bibsync-0.3.1 → bibsync-0.3.3}/PKG-INFO +22 -7
- {bibsync-0.3.1 → bibsync-0.3.3}/README.md +21 -6
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/api/index.md +15 -1
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/user-guide/pre-commit.md +7 -2
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/user-guide/usage.md +31 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/python/bibsync/__init__.pyi +1 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/src/cli.rs +4 -1
- {bibsync-0.3.1 → bibsync-0.3.3}/src/lib.rs +415 -46
- {bibsync-0.3.1 → bibsync-0.3.3}/src/python.rs +11 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/tests/cli.rs +41 -1
- {bibsync-0.3.1 → bibsync-0.3.3}/tests/python/test_bindings.py +1 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.gitignore +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.markdownlint.yaml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.pre-commit-config.yaml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.pre-commit-hooks.yaml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.prettierrc +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/.typos.toml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/CODE_OF_CONDUCT.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/CONTRIBUTING.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/LICENSE +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/SECURITY.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/SUPPORT.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/benches/greeting.rs +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/cliff.toml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/deny.toml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/contributing.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/index.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/security.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/user-guide/examples.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/user-guide/installation.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/docs/user-guide/providers.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/README.md +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/greet.rs +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/inspire-main.bib +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/inspire-main.tex +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/main.bib +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/examples/main.tex +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/pyproject.toml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/python/bibsync/__init__.py +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/python/bibsync/__main__.py +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/python/bibsync/py.typed +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/renovate.json +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/rust-toolchain.toml +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/scripts/pre-commit-bibsync +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/setup_repo.sh +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/src/main.rs +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/tests/examples.rs +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/uv.lock +0 -0
- {bibsync-0.3.1 → bibsync-0.3.3}/zensical.toml +0 -0
|
@@ -124,7 +124,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
|
124
124
|
|
|
125
125
|
[[package]]
|
|
126
126
|
name = "bibsync"
|
|
127
|
-
version = "0.3.
|
|
127
|
+
version = "0.3.3"
|
|
128
128
|
dependencies = [
|
|
129
129
|
"assert_cmd",
|
|
130
130
|
"clap",
|
|
@@ -170,9 +170,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|
|
170
170
|
|
|
171
171
|
[[package]]
|
|
172
172
|
name = "cc"
|
|
173
|
-
version = "1.2.
|
|
173
|
+
version = "1.2.63"
|
|
174
174
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
175
|
-
checksum = "
|
|
175
|
+
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
|
|
176
176
|
dependencies = [
|
|
177
177
|
"find-msvc-tools",
|
|
178
178
|
"jobserver",
|
|
@@ -537,9 +537,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
|
|
537
537
|
|
|
538
538
|
[[package]]
|
|
539
539
|
name = "hyper"
|
|
540
|
-
version = "1.10.
|
|
540
|
+
version = "1.10.1"
|
|
541
541
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
542
|
-
checksum = "
|
|
542
|
+
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
|
543
543
|
dependencies = [
|
|
544
544
|
"atomic-waker",
|
|
545
545
|
"bytes",
|
|
@@ -1270,9 +1270,9 @@ dependencies = [
|
|
|
1270
1270
|
|
|
1271
1271
|
[[package]]
|
|
1272
1272
|
name = "rustls-native-certs"
|
|
1273
|
-
version = "0.8.
|
|
1273
|
+
version = "0.8.4"
|
|
1274
1274
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1275
|
-
checksum = "
|
|
1275
|
+
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
|
1276
1276
|
dependencies = [
|
|
1277
1277
|
"openssl-probe",
|
|
1278
1278
|
"rustls-pki-types",
|
|
@@ -1445,9 +1445,9 @@ dependencies = [
|
|
|
1445
1445
|
|
|
1446
1446
|
[[package]]
|
|
1447
1447
|
name = "shlex"
|
|
1448
|
-
version = "
|
|
1448
|
+
version = "2.0.1"
|
|
1449
1449
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1450
|
-
checksum = "
|
|
1450
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
1451
1451
|
|
|
1452
1452
|
[[package]]
|
|
1453
1453
|
name = "simd_cesu8"
|
|
@@ -2218,18 +2218,18 @@ dependencies = [
|
|
|
2218
2218
|
|
|
2219
2219
|
[[package]]
|
|
2220
2220
|
name = "zerocopy"
|
|
2221
|
-
version = "0.8.
|
|
2221
|
+
version = "0.8.50"
|
|
2222
2222
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2223
|
-
checksum = "
|
|
2223
|
+
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
|
|
2224
2224
|
dependencies = [
|
|
2225
2225
|
"zerocopy-derive",
|
|
2226
2226
|
]
|
|
2227
2227
|
|
|
2228
2228
|
[[package]]
|
|
2229
2229
|
name = "zerocopy-derive"
|
|
2230
|
-
version = "0.8.
|
|
2230
|
+
version = "0.8.50"
|
|
2231
2231
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2232
|
-
checksum = "
|
|
2232
|
+
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
|
|
2233
2233
|
dependencies = [
|
|
2234
2234
|
"proc-macro2",
|
|
2235
2235
|
"quote",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bibsync
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -65,6 +65,11 @@ NASA ADS and/or InspireHEP, rewrites provider BibTeX entries so the citekey stay
|
|
|
65
65
|
the key used in TeX, and reports whether the output `.bib` file is current. With
|
|
66
66
|
`--fix`, it writes the merged bibliography.
|
|
67
67
|
|
|
68
|
+
When a citekey cannot be resolved, the command prints the key together with a
|
|
69
|
+
reason and the likely fix. For example, an unsupported citekey is reported as an
|
|
70
|
+
identifier-format problem, while an arXiv ID or DOI that the provider cannot find
|
|
71
|
+
is reported as a provider miss.
|
|
72
|
+
|
|
68
73
|
## Installation
|
|
69
74
|
|
|
70
75
|
Install the Rust CLI from crates.io:
|
|
@@ -112,9 +117,9 @@ For convenience, you can also use:
|
|
|
112
117
|
@software{wong2026bibsync,
|
|
113
118
|
author = {Wong, Isaac C. F.},
|
|
114
119
|
title = {bibsync: A Rust package to automatically resolve, synchronize, and validate LaTeX citations across BibTeX databases},
|
|
115
|
-
version = {v0.3.
|
|
120
|
+
version = {v0.3.3},
|
|
116
121
|
year = {2026},
|
|
117
|
-
month =
|
|
122
|
+
month = jun,
|
|
118
123
|
doi = {10.5281/zenodo.20422622},
|
|
119
124
|
url = {https://doi.org/10.5281/zenodo.20422622}
|
|
120
125
|
}
|
|
@@ -193,6 +198,12 @@ To update a bibliography in place, pass a single `.bib` file:
|
|
|
193
198
|
bibsync --fix references.bib --force-regenerate
|
|
194
199
|
```
|
|
195
200
|
|
|
201
|
+
Existing input files are validated before resolution. A missing single `.bib`
|
|
202
|
+
input, `--other` bibliography, or `--ignore-file` is reported as an error with
|
|
203
|
+
the path that could not be read. Existing bibliography files are also parsed
|
|
204
|
+
strictly, so malformed BibTeX reports the file and the approximate failing
|
|
205
|
+
entry instead of being treated as an empty or partial bibliography.
|
|
206
|
+
|
|
196
207
|
### Update Behavior
|
|
197
208
|
|
|
198
209
|
By default `bibsync` leaves published entries untouched. Only entries that look
|
|
@@ -244,6 +255,10 @@ bibsync --fix --refresh-cache main.tex -o references.bib
|
|
|
244
255
|
|
|
245
256
|
Override the cache location with `--cache-dir DIR`.
|
|
246
257
|
|
|
258
|
+
If a cache file is corrupt, `bibsync` reports the exact cache path and asks you
|
|
259
|
+
to refresh or remove the bad cache entry. Provider request failures include the
|
|
260
|
+
provider and citekey or batch being resolved.
|
|
261
|
+
|
|
247
262
|
## Pre-commit
|
|
248
263
|
|
|
249
264
|
The repository includes `.pre-commit-hooks.yaml`, so other projects can use
|
|
@@ -254,7 +269,7 @@ Use the pre-built binary hook for faster installs:
|
|
|
254
269
|
```yaml
|
|
255
270
|
repos:
|
|
256
271
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
257
|
-
rev: v0.3.
|
|
272
|
+
rev: v0.3.3
|
|
258
273
|
hooks:
|
|
259
274
|
- id: bibsync-bin
|
|
260
275
|
args: [--cache, --provider, inspire, --output, references.bib]
|
|
@@ -269,7 +284,7 @@ installation:
|
|
|
269
284
|
```yaml
|
|
270
285
|
repos:
|
|
271
286
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
272
|
-
rev: v0.3.
|
|
287
|
+
rev: v0.3.3
|
|
273
288
|
hooks:
|
|
274
289
|
- id: bibsync
|
|
275
290
|
args: [--provider, inspire, --output, references.bib]
|
|
@@ -281,7 +296,7 @@ writing changes. To let the hook update files, add `--fix` to the hook args:
|
|
|
281
296
|
```yaml
|
|
282
297
|
repos:
|
|
283
298
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
284
|
-
rev: v0.3.
|
|
299
|
+
rev: v0.3.3
|
|
285
300
|
hooks:
|
|
286
301
|
- id: bibsync-bin
|
|
287
302
|
args: [--fix, --cache, --provider, inspire, --output, references.bib]
|
|
@@ -292,7 +307,7 @@ To skip manually curated entries, add `--ignore-file`:
|
|
|
292
307
|
```yaml
|
|
293
308
|
repos:
|
|
294
309
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
295
|
-
rev: v0.3.
|
|
310
|
+
rev: v0.3.3
|
|
296
311
|
hooks:
|
|
297
312
|
- id: bibsync-bin
|
|
298
313
|
args:
|
|
@@ -38,6 +38,11 @@ NASA ADS and/or InspireHEP, rewrites provider BibTeX entries so the citekey stay
|
|
|
38
38
|
the key used in TeX, and reports whether the output `.bib` file is current. With
|
|
39
39
|
`--fix`, it writes the merged bibliography.
|
|
40
40
|
|
|
41
|
+
When a citekey cannot be resolved, the command prints the key together with a
|
|
42
|
+
reason and the likely fix. For example, an unsupported citekey is reported as an
|
|
43
|
+
identifier-format problem, while an arXiv ID or DOI that the provider cannot find
|
|
44
|
+
is reported as a provider miss.
|
|
45
|
+
|
|
41
46
|
## Installation
|
|
42
47
|
|
|
43
48
|
Install the Rust CLI from crates.io:
|
|
@@ -85,9 +90,9 @@ For convenience, you can also use:
|
|
|
85
90
|
@software{wong2026bibsync,
|
|
86
91
|
author = {Wong, Isaac C. F.},
|
|
87
92
|
title = {bibsync: A Rust package to automatically resolve, synchronize, and validate LaTeX citations across BibTeX databases},
|
|
88
|
-
version = {v0.3.
|
|
93
|
+
version = {v0.3.3},
|
|
89
94
|
year = {2026},
|
|
90
|
-
month =
|
|
95
|
+
month = jun,
|
|
91
96
|
doi = {10.5281/zenodo.20422622},
|
|
92
97
|
url = {https://doi.org/10.5281/zenodo.20422622}
|
|
93
98
|
}
|
|
@@ -166,6 +171,12 @@ To update a bibliography in place, pass a single `.bib` file:
|
|
|
166
171
|
bibsync --fix references.bib --force-regenerate
|
|
167
172
|
```
|
|
168
173
|
|
|
174
|
+
Existing input files are validated before resolution. A missing single `.bib`
|
|
175
|
+
input, `--other` bibliography, or `--ignore-file` is reported as an error with
|
|
176
|
+
the path that could not be read. Existing bibliography files are also parsed
|
|
177
|
+
strictly, so malformed BibTeX reports the file and the approximate failing
|
|
178
|
+
entry instead of being treated as an empty or partial bibliography.
|
|
179
|
+
|
|
169
180
|
### Update Behavior
|
|
170
181
|
|
|
171
182
|
By default `bibsync` leaves published entries untouched. Only entries that look
|
|
@@ -217,6 +228,10 @@ bibsync --fix --refresh-cache main.tex -o references.bib
|
|
|
217
228
|
|
|
218
229
|
Override the cache location with `--cache-dir DIR`.
|
|
219
230
|
|
|
231
|
+
If a cache file is corrupt, `bibsync` reports the exact cache path and asks you
|
|
232
|
+
to refresh or remove the bad cache entry. Provider request failures include the
|
|
233
|
+
provider and citekey or batch being resolved.
|
|
234
|
+
|
|
220
235
|
## Pre-commit
|
|
221
236
|
|
|
222
237
|
The repository includes `.pre-commit-hooks.yaml`, so other projects can use
|
|
@@ -227,7 +242,7 @@ Use the pre-built binary hook for faster installs:
|
|
|
227
242
|
```yaml
|
|
228
243
|
repos:
|
|
229
244
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
230
|
-
rev: v0.3.
|
|
245
|
+
rev: v0.3.3
|
|
231
246
|
hooks:
|
|
232
247
|
- id: bibsync-bin
|
|
233
248
|
args: [--cache, --provider, inspire, --output, references.bib]
|
|
@@ -242,7 +257,7 @@ installation:
|
|
|
242
257
|
```yaml
|
|
243
258
|
repos:
|
|
244
259
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
245
|
-
rev: v0.3.
|
|
260
|
+
rev: v0.3.3
|
|
246
261
|
hooks:
|
|
247
262
|
- id: bibsync
|
|
248
263
|
args: [--provider, inspire, --output, references.bib]
|
|
@@ -254,7 +269,7 @@ writing changes. To let the hook update files, add `--fix` to the hook args:
|
|
|
254
269
|
```yaml
|
|
255
270
|
repos:
|
|
256
271
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
257
|
-
rev: v0.3.
|
|
272
|
+
rev: v0.3.3
|
|
258
273
|
hooks:
|
|
259
274
|
- id: bibsync-bin
|
|
260
275
|
args: [--fix, --cache, --provider, inspire, --output, references.bib]
|
|
@@ -265,7 +280,7 @@ To skip manually curated entries, add `--ignore-file`:
|
|
|
265
280
|
```yaml
|
|
266
281
|
repos:
|
|
267
282
|
- repo: https://github.com/isaac-cf-wong/bibsync
|
|
268
|
-
rev: v0.3.
|
|
283
|
+
rev: v0.3.3
|
|
269
284
|
hooks:
|
|
270
285
|
- id: bibsync-bin
|
|
271
286
|
args:
|
|
@@ -54,11 +54,15 @@ behavior.
|
|
|
54
54
|
- `output`: explicit output bibliography file.
|
|
55
55
|
- `other_bibliographies`: read-only bibliography sources.
|
|
56
56
|
- `provider`: `auto`, NASA ADS, or InspireHEP.
|
|
57
|
-
- `
|
|
57
|
+
- `update_mode`: controls whether existing entries are refreshed.
|
|
58
58
|
- `force_regenerate`: rewrite existing entries from provider output.
|
|
59
59
|
- `merge_other`: copy matching read-only entries into the output file.
|
|
60
60
|
- `backup`: create a `.bak` file before overwriting.
|
|
61
61
|
- `check`: compare only and do not write.
|
|
62
|
+
- `cache`: read and write provider responses from the local cache.
|
|
63
|
+
- `refresh_cache`: bypass cache reads and update cached provider responses.
|
|
64
|
+
- `cache_dir`: override the cache location.
|
|
65
|
+
- `ignore_file`: citekeys to skip during resolution.
|
|
62
66
|
|
|
63
67
|
`SyncReport` describes the outcome:
|
|
64
68
|
|
|
@@ -67,9 +71,19 @@ behavior.
|
|
|
67
71
|
- `existing`: citekeys already present.
|
|
68
72
|
- `found_in_other`: citekeys found in read-only bibliographies.
|
|
69
73
|
- `unresolved`: citekeys that could not be resolved.
|
|
74
|
+
- `unresolved_details`: per-citekey diagnostics explaining whether resolution
|
|
75
|
+
failed because the key is not a supported identifier or because the provider
|
|
76
|
+
returned no match.
|
|
70
77
|
- `changed`: whether the output would change.
|
|
71
78
|
- `check_mode`: whether the run was check-only.
|
|
72
79
|
|
|
80
|
+
`sync_files` validates existing input files before resolution. Missing TeX
|
|
81
|
+
inputs, missing single-file `.bib` inputs, missing `other_bibliographies`,
|
|
82
|
+
missing `ignore_file`, malformed existing BibTeX, corrupt cache JSON, and
|
|
83
|
+
provider request failures are returned as `BibsyncError` values with file,
|
|
84
|
+
provider, and key context where available. A missing output bibliography is
|
|
85
|
+
allowed when `output` points to the file that should be created.
|
|
86
|
+
|
|
73
87
|
## Providers
|
|
74
88
|
|
|
75
89
|
The built-in providers are:
|
|
@@ -115,9 +115,14 @@ bibsync --fix --cache --provider inspire --output references.bib main.tex
|
|
|
115
115
|
Then review and commit the changed bibliography.
|
|
116
116
|
|
|
117
117
|
If the hook reports unresolved citekeys, either correct the citekey or choose a
|
|
118
|
-
provider that supports that identifier type.
|
|
119
|
-
|
|
118
|
+
provider that supports that identifier type. The hook output includes a reason
|
|
119
|
+
for each unresolved key, such as an unsupported identifier format or a provider
|
|
120
|
+
miss. For example, ADS bibcodes require NASA ADS:
|
|
120
121
|
|
|
121
122
|
```shell
|
|
122
123
|
bibsync --fix --cache --provider ads --output references.bib main.tex
|
|
123
124
|
```
|
|
125
|
+
|
|
126
|
+
Missing `--ignore-file` or `--other` paths and malformed existing BibTeX are
|
|
127
|
+
reported as input errors with the affected path. Fix those files or hook
|
|
128
|
+
arguments before rerunning the hook.
|
|
@@ -21,6 +21,13 @@ reports whether `references.bib` is current. It does not write changes unless
|
|
|
21
21
|
bibsync --fix main.tex -o references.bib
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
When a required citekey cannot be resolved, `bibsync` prints a diagnostic for
|
|
25
|
+
each key. Unsupported citekeys are reported as identifier-format problems, and
|
|
26
|
+
identifier-like keys that the selected provider cannot find are reported as
|
|
27
|
+
provider misses. Those messages are meant to be actionable in automation: fix the
|
|
28
|
+
citekey, choose a provider that supports it, add the entry manually, or add the
|
|
29
|
+
key to an ignore file.
|
|
30
|
+
|
|
24
31
|
## Citekey Style
|
|
25
32
|
|
|
26
33
|
The most reliable workflow is to cite by identifier:
|
|
@@ -101,6 +108,10 @@ Additional read-only bibliographies can be passed with `--other`:
|
|
|
101
108
|
bibsync main.tex -o references.bib --other shared.bib software.bib
|
|
102
109
|
```
|
|
103
110
|
|
|
111
|
+
Every file passed with `--other` must already exist and contain valid BibTeX.
|
|
112
|
+
If a path is wrong or a file is malformed, `bibsync` stops with an error naming
|
|
113
|
+
that file instead of silently ignoring it.
|
|
114
|
+
|
|
104
115
|
By default, entries found in those read-only files are not copied into the main
|
|
105
116
|
output file. This is useful when a project deliberately keeps shared references
|
|
106
117
|
or software citations in separate files.
|
|
@@ -132,6 +143,9 @@ bibsync --fix main.tex -o references.bib --ignore-file .bibsyncignore
|
|
|
132
143
|
Ignored citekeys are never sent to any provider and their existing bib entries
|
|
133
144
|
are never modified.
|
|
134
145
|
|
|
146
|
+
The ignore file must exist when `--ignore-file` is passed. A misspelled ignore
|
|
147
|
+
path is reported as a missing input file.
|
|
148
|
+
|
|
135
149
|
## Updating A BibTeX File
|
|
136
150
|
|
|
137
151
|
Passing a single `.bib` file uses the existing keys in that bibliography as the
|
|
@@ -148,6 +162,10 @@ scanning TeX files. Add `--fix` to refresh the file:
|
|
|
148
162
|
bibsync --fix references.bib
|
|
149
163
|
```
|
|
150
164
|
|
|
165
|
+
The `.bib` file must exist in this mode. Existing output bibliographies are also
|
|
166
|
+
parsed before resolution; malformed BibTeX reports the file and approximate
|
|
167
|
+
entry location so the bibliography can be corrected before running again.
|
|
168
|
+
|
|
151
169
|
Use `--force-regenerate` to rewrite all existing entries from provider output:
|
|
152
170
|
|
|
153
171
|
```shell
|
|
@@ -167,6 +185,14 @@ The command exits with a non-zero status when the bibliography would change or
|
|
|
167
185
|
when a required citekey cannot be resolved. This is the mode used by the
|
|
168
186
|
pre-commit hook.
|
|
169
187
|
|
|
188
|
+
Unresolved citekeys are printed with reasons. For example:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
unresolved:
|
|
192
|
+
Smith2024: unsupported identifier format; use an arXiv ID, DOI, or ADS bibcode, or add the entry to the bibliography or ignore file
|
|
193
|
+
2404.14498: provider returned no matching BibTeX entry; check the citekey, choose a provider that supports it, or add the entry manually
|
|
194
|
+
```
|
|
195
|
+
|
|
170
196
|
Use `--fix` to write the calculated bibliography:
|
|
171
197
|
|
|
172
198
|
```shell
|
|
@@ -205,6 +231,11 @@ Override the cache location with `--cache-dir`:
|
|
|
205
231
|
bibsync --cache --cache-dir .bibsync-cache main.tex -o references.bib
|
|
206
232
|
```
|
|
207
233
|
|
|
234
|
+
If a cached JSON file is corrupt, `bibsync` reports the exact cache path. Remove
|
|
235
|
+
that file or rerun with `--refresh-cache` to rebuild the provider response.
|
|
236
|
+
Network and provider failures include the provider and citekey or batch being
|
|
237
|
+
resolved.
|
|
238
|
+
|
|
208
239
|
## Backups
|
|
209
240
|
|
|
210
241
|
When `bibsync` writes over an existing bibliography, it creates a `.bak` file by
|
|
@@ -165,7 +165,10 @@ where
|
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
if !report.unresolved.is_empty() {
|
|
168
|
-
println!("unresolved:
|
|
168
|
+
println!("unresolved:");
|
|
169
|
+
for detail in &report.unresolved_details {
|
|
170
|
+
println!(" {}: {}", detail.key, detail.reason);
|
|
171
|
+
}
|
|
169
172
|
}
|
|
170
173
|
if report.changed {
|
|
171
174
|
if report.check_mode {
|
|
@@ -36,21 +36,57 @@ pub enum BibsyncError {
|
|
|
36
36
|
/// The underlying I/O error.
|
|
37
37
|
source: io::Error,
|
|
38
38
|
},
|
|
39
|
+
/// An expected input file was not found.
|
|
40
|
+
#[error("{role} not found: {path}")]
|
|
41
|
+
MissingInput {
|
|
42
|
+
/// File path associated with the failure.
|
|
43
|
+
path: PathBuf,
|
|
44
|
+
/// User-facing description of the missing input.
|
|
45
|
+
role: &'static str,
|
|
46
|
+
},
|
|
39
47
|
/// An HTTP request failed.
|
|
40
48
|
#[error("HTTP request failed: {0}")]
|
|
41
49
|
Http(#[from] reqwest::Error),
|
|
50
|
+
/// A provider request failed while resolving a specific key.
|
|
51
|
+
#[error("{provider} request failed while resolving {key}: {source}")]
|
|
52
|
+
ProviderRequest {
|
|
53
|
+
/// Provider name.
|
|
54
|
+
provider: &'static str,
|
|
55
|
+
/// Citekey or identifier being resolved.
|
|
56
|
+
key: String,
|
|
57
|
+
/// The underlying request error.
|
|
58
|
+
source: reqwest::Error,
|
|
59
|
+
},
|
|
42
60
|
/// NASA ADS was selected without an API token.
|
|
43
61
|
#[error("NASA ADS requires ADS_API_TOKEN")]
|
|
44
62
|
MissingAdsToken,
|
|
45
63
|
/// An HTTP header value could not be built.
|
|
46
64
|
#[error("invalid HTTP header value: {0}")]
|
|
47
65
|
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
|
|
48
|
-
/// JSON cache
|
|
49
|
-
#[error("JSON
|
|
50
|
-
Json
|
|
66
|
+
/// JSON cache parsing failed.
|
|
67
|
+
#[error("{path}: invalid JSON cache file: {source}")]
|
|
68
|
+
Json {
|
|
69
|
+
/// Cache file path associated with the failure.
|
|
70
|
+
path: PathBuf,
|
|
71
|
+
/// The underlying JSON error.
|
|
72
|
+
source: serde_json::Error,
|
|
73
|
+
},
|
|
74
|
+
/// JSON cache serialization failed.
|
|
75
|
+
#[error("could not serialize JSON cache record: {0}")]
|
|
76
|
+
JsonSerialization(#[from] serde_json::Error),
|
|
51
77
|
/// An input file did not point to any BibTeX output file.
|
|
52
|
-
#[error(
|
|
78
|
+
#[error(
|
|
79
|
+
"could not identify a bibliography file; pass --output or add a \\bibliography{{...}} command"
|
|
80
|
+
)]
|
|
53
81
|
MissingOutput,
|
|
82
|
+
/// A bibliography file could not be parsed.
|
|
83
|
+
#[error("{path}: invalid BibTeX: {message}")]
|
|
84
|
+
InvalidBibtex {
|
|
85
|
+
/// File path associated with the failure.
|
|
86
|
+
path: PathBuf,
|
|
87
|
+
/// User-facing parse diagnostic.
|
|
88
|
+
message: String,
|
|
89
|
+
},
|
|
54
90
|
/// A provider returned an invalid BibTeX payload.
|
|
55
91
|
#[error("{provider} did not return a usable BibTeX entry for {key}")]
|
|
56
92
|
InvalidProviderBibtex {
|
|
@@ -149,12 +185,59 @@ pub struct SyncReport {
|
|
|
149
185
|
pub found_in_other: Vec<String>,
|
|
150
186
|
/// Citekeys that could not be resolved.
|
|
151
187
|
pub unresolved: Vec<String>,
|
|
188
|
+
/// Detailed diagnostics for citekeys that could not be resolved.
|
|
189
|
+
pub unresolved_details: Vec<UnresolvedCitation>,
|
|
152
190
|
/// Whether the output file content changed.
|
|
153
191
|
pub changed: bool,
|
|
154
192
|
/// Whether changes were only reported because check mode was enabled.
|
|
155
193
|
pub check_mode: bool,
|
|
156
194
|
}
|
|
157
195
|
|
|
196
|
+
/// Diagnostic for one citekey that could not be resolved.
|
|
197
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
198
|
+
pub struct UnresolvedCitation {
|
|
199
|
+
/// Citekey or identifier that could not be resolved.
|
|
200
|
+
pub key: String,
|
|
201
|
+
/// Reason the key could not be resolved automatically.
|
|
202
|
+
pub reason: UnresolvedReason,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Reason a citekey could not be resolved.
|
|
206
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
207
|
+
pub enum UnresolvedReason {
|
|
208
|
+
/// The key is not an arXiv ID, DOI, or ADS bibcode.
|
|
209
|
+
UnsupportedIdentifier,
|
|
210
|
+
/// The selected provider did not return a matching entry.
|
|
211
|
+
ProviderNoMatch,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
impl UnresolvedReason {
|
|
215
|
+
fn explanation(self) -> &'static str {
|
|
216
|
+
match self {
|
|
217
|
+
Self::UnsupportedIdentifier => {
|
|
218
|
+
"unsupported identifier format; use an arXiv ID, DOI, or ADS bibcode, or add the entry to the bibliography or ignore file"
|
|
219
|
+
}
|
|
220
|
+
Self::ProviderNoMatch => {
|
|
221
|
+
"provider returned no matching BibTeX entry; check the citekey, choose a provider that supports it, or add the entry manually"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl std::fmt::Display for UnresolvedReason {
|
|
228
|
+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
229
|
+
formatter.write_str(self.explanation())
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
impl SyncReport {
|
|
234
|
+
fn push_unresolved(&mut self, key: String, reason: UnresolvedReason) {
|
|
235
|
+
self.unresolved.push(key.clone());
|
|
236
|
+
self.unresolved_details
|
|
237
|
+
.push(UnresolvedCitation { key, reason });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
158
241
|
/// A resolved BibTeX entry.
|
|
159
242
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
160
243
|
pub struct ResolvedEntry {
|
|
@@ -241,7 +324,7 @@ pub fn sync_files_with_provider(
|
|
|
241
324
|
let bib_update_mode = files.len() == 1 && has_extension(&files[0], "bib");
|
|
242
325
|
let (keys, output, discovered_other) = if bib_update_mode {
|
|
243
326
|
let output = files[0].clone();
|
|
244
|
-
let bib = Bibliography::
|
|
327
|
+
let bib = Bibliography::read_existing(&output, "bibliography input")?;
|
|
245
328
|
(bib.keys(), output, Vec::new())
|
|
246
329
|
} else {
|
|
247
330
|
let tex_scan = scan_tex_files(files)?;
|
|
@@ -268,10 +351,10 @@ pub fn sync_files_with_provider(
|
|
|
268
351
|
};
|
|
269
352
|
|
|
270
353
|
let original = read_to_string_optional(&output)?;
|
|
271
|
-
let mut bibliography = Bibliography::
|
|
354
|
+
let mut bibliography = Bibliography::parse_path(&output, &original)?;
|
|
272
355
|
let mut other_bibliography = Bibliography::default();
|
|
273
356
|
for path in &other_paths {
|
|
274
|
-
other_bibliography.merge(Bibliography::
|
|
357
|
+
other_bibliography.merge(Bibliography::read_existing(path, "read-only bibliography")?);
|
|
275
358
|
}
|
|
276
359
|
|
|
277
360
|
let ignore_set = if let Some(ref path) = options.ignore_file {
|
|
@@ -326,7 +409,7 @@ pub fn sync_files_with_provider(
|
|
|
326
409
|
if exists {
|
|
327
410
|
report.existing.push(key);
|
|
328
411
|
} else {
|
|
329
|
-
report.
|
|
412
|
+
report.push_unresolved(key, UnresolvedReason::UnsupportedIdentifier);
|
|
330
413
|
}
|
|
331
414
|
continue;
|
|
332
415
|
}
|
|
@@ -358,7 +441,7 @@ pub fn sync_files_with_provider(
|
|
|
358
441
|
if exists {
|
|
359
442
|
report.existing.push(key.clone());
|
|
360
443
|
} else {
|
|
361
|
-
report.
|
|
444
|
+
report.push_unresolved(key.clone(), UnresolvedReason::ProviderNoMatch);
|
|
362
445
|
}
|
|
363
446
|
continue;
|
|
364
447
|
};
|
|
@@ -413,6 +496,9 @@ pub fn sync_files_with_provider(
|
|
|
413
496
|
report.existing.sort();
|
|
414
497
|
report.found_in_other.sort();
|
|
415
498
|
report.unresolved.sort();
|
|
499
|
+
report
|
|
500
|
+
.unresolved_details
|
|
501
|
+
.sort_by(|left, right| left.key.cmp(&right.key));
|
|
416
502
|
Ok(report)
|
|
417
503
|
}
|
|
418
504
|
|
|
@@ -556,27 +642,38 @@ struct Bibliography {
|
|
|
556
642
|
}
|
|
557
643
|
|
|
558
644
|
impl Bibliography {
|
|
559
|
-
fn
|
|
560
|
-
|
|
645
|
+
fn read_existing(path: &Path, role: &'static str) -> Result<Self> {
|
|
646
|
+
Self::parse_path(path, &read_to_string_existing(path, role)?)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn parse_path(path: &Path, input: &str) -> Result<Self> {
|
|
650
|
+
Self::try_parse(input).map_err(|message| BibsyncError::InvalidBibtex {
|
|
651
|
+
path: path.to_owned(),
|
|
652
|
+
message,
|
|
653
|
+
})
|
|
561
654
|
}
|
|
562
655
|
|
|
563
|
-
fn
|
|
656
|
+
fn try_parse(input: &str) -> std::result::Result<Self, String> {
|
|
564
657
|
let mut bibliography = Self::default();
|
|
565
658
|
let mut first_entry_start = None;
|
|
566
|
-
for segment in split_bib_entries(input) {
|
|
659
|
+
for segment in split_bib_entries(input)? {
|
|
567
660
|
if first_entry_start.is_none() {
|
|
568
661
|
first_entry_start = input.find(segment);
|
|
569
662
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
663
|
+
let entry = BibEntry::parse(segment).ok_or_else(|| {
|
|
664
|
+
format!(
|
|
665
|
+
"could not parse entry starting near line {}",
|
|
666
|
+
line_number(input, input.find(segment).unwrap_or(0))
|
|
667
|
+
)
|
|
668
|
+
})?;
|
|
669
|
+
bibliography.entries.insert(entry.key.clone(), entry);
|
|
573
670
|
}
|
|
574
671
|
if let Some(index) = first_entry_start {
|
|
575
672
|
input[..index].trim().clone_into(&mut bibliography.preamble);
|
|
576
673
|
} else {
|
|
577
674
|
input.trim().clone_into(&mut bibliography.preamble);
|
|
578
675
|
}
|
|
579
|
-
bibliography
|
|
676
|
+
Ok(bibliography)
|
|
580
677
|
}
|
|
581
678
|
|
|
582
679
|
fn contains(&self, key: &str) -> bool {
|
|
@@ -617,16 +714,16 @@ impl std::fmt::Display for Bibliography {
|
|
|
617
714
|
}
|
|
618
715
|
}
|
|
619
716
|
|
|
620
|
-
fn split_bib_entries(input: &str) -> Vec<&str> {
|
|
717
|
+
fn split_bib_entries(input: &str) -> std::result::Result<Vec<&str>, String> {
|
|
621
718
|
let mut entries = Vec::new();
|
|
622
719
|
let bytes = input.as_bytes();
|
|
623
720
|
let mut index = 0;
|
|
624
721
|
while let Some(relative_at) = input[index..].find('@') {
|
|
625
722
|
let start = index + relative_at;
|
|
626
|
-
let Some(
|
|
627
|
-
|
|
723
|
+
let Some(open) = bib_entry_open(input, start) else {
|
|
724
|
+
index = start + 1;
|
|
725
|
+
continue;
|
|
628
726
|
};
|
|
629
|
-
let open = start + relative_open;
|
|
630
727
|
let close = if bytes.get(open) == Some(&b'{') {
|
|
631
728
|
b'}'
|
|
632
729
|
} else {
|
|
@@ -649,11 +746,42 @@ fn split_bib_entries(input: &str) -> Vec<&str> {
|
|
|
649
746
|
if let Some(end) = end {
|
|
650
747
|
entries.push(&input[start..end]);
|
|
651
748
|
index = end;
|
|
749
|
+
} else {
|
|
750
|
+
return Err(format!(
|
|
751
|
+
"entry starting near line {} is missing a closing '{}'",
|
|
752
|
+
line_number(input, start),
|
|
753
|
+
close as char
|
|
754
|
+
));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
Ok(entries)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
fn bib_entry_open(input: &str, at_index: usize) -> Option<usize> {
|
|
761
|
+
let rest = input.get(at_index + 1..)?;
|
|
762
|
+
let mut type_end = 0;
|
|
763
|
+
for (offset, ch) in rest.char_indices() {
|
|
764
|
+
if ch.is_ascii_alphabetic() {
|
|
765
|
+
type_end = offset + ch.len_utf8();
|
|
652
766
|
} else {
|
|
653
767
|
break;
|
|
654
768
|
}
|
|
655
769
|
}
|
|
656
|
-
|
|
770
|
+
if type_end == 0 {
|
|
771
|
+
return None;
|
|
772
|
+
}
|
|
773
|
+
let after_type = &rest[type_end..];
|
|
774
|
+
let whitespace = after_type.len() - after_type.trim_start().len();
|
|
775
|
+
let open = at_index + 1 + type_end + whitespace;
|
|
776
|
+
matches!(input.as_bytes().get(open), Some(b'{' | b'(')).then_some(open)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
fn line_number(input: &str, byte_index: usize) -> usize {
|
|
780
|
+
input[..byte_index.min(input.len())]
|
|
781
|
+
.bytes()
|
|
782
|
+
.filter(|byte| *byte == b'\n')
|
|
783
|
+
.count()
|
|
784
|
+
+ 1
|
|
657
785
|
}
|
|
658
786
|
|
|
659
787
|
fn indent_body(body: &str) -> String {
|
|
@@ -699,12 +827,28 @@ fn read_to_string_optional(path: &Path) -> Result<String> {
|
|
|
699
827
|
}
|
|
700
828
|
}
|
|
701
829
|
|
|
830
|
+
fn read_to_string_existing(path: &Path, role: &'static str) -> Result<String> {
|
|
831
|
+
match fs::read_to_string(path) {
|
|
832
|
+
Ok(content) => Ok(content),
|
|
833
|
+
Err(source) if source.kind() == io::ErrorKind::NotFound => {
|
|
834
|
+
Err(BibsyncError::MissingInput {
|
|
835
|
+
path: path.to_owned(),
|
|
836
|
+
role,
|
|
837
|
+
})
|
|
838
|
+
}
|
|
839
|
+
Err(source) => Err(BibsyncError::Io {
|
|
840
|
+
path: path.to_owned(),
|
|
841
|
+
source,
|
|
842
|
+
}),
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
702
846
|
fn is_supported_identifier(key: &str) -> bool {
|
|
703
847
|
is_arxiv_id(key) || is_doi(key) || is_ads_bibcode(key)
|
|
704
848
|
}
|
|
705
849
|
|
|
706
850
|
fn load_ignore_set(path: &Path) -> Result<BTreeSet<String>> {
|
|
707
|
-
let content =
|
|
851
|
+
let content = read_to_string_existing(path, "ignore file")?;
|
|
708
852
|
Ok(content
|
|
709
853
|
.lines()
|
|
710
854
|
.map(str::trim)
|
|
@@ -962,7 +1106,14 @@ fn cache_store(root: &Path, key: &str, entry: &ResolvedEntry) -> Result<()> {
|
|
|
962
1106
|
|
|
963
1107
|
fn read_json_optional<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<Option<T>> {
|
|
964
1108
|
match fs::read_to_string(path) {
|
|
965
|
-
Ok(content) =>
|
|
1109
|
+
Ok(content) => {
|
|
1110
|
+
serde_json::from_str(&content)
|
|
1111
|
+
.map(Some)
|
|
1112
|
+
.map_err(|source| BibsyncError::Json {
|
|
1113
|
+
path: path.to_owned(),
|
|
1114
|
+
source,
|
|
1115
|
+
})
|
|
1116
|
+
}
|
|
966
1117
|
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
|
|
967
1118
|
Err(source) => Err(BibsyncError::Io {
|
|
968
1119
|
path: path.to_owned(),
|
|
@@ -1038,6 +1189,18 @@ fn provider_name_from_slug(slug: &str) -> &'static str {
|
|
|
1038
1189
|
}
|
|
1039
1190
|
}
|
|
1040
1191
|
|
|
1192
|
+
fn provider_request<T>(
|
|
1193
|
+
provider: &'static str,
|
|
1194
|
+
key: impl Into<String>,
|
|
1195
|
+
result: std::result::Result<T, reqwest::Error>,
|
|
1196
|
+
) -> Result<T> {
|
|
1197
|
+
result.map_err(|source| BibsyncError::ProviderRequest {
|
|
1198
|
+
provider,
|
|
1199
|
+
key: key.into(),
|
|
1200
|
+
source,
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1041
1204
|
fn unix_timestamp() -> u64 {
|
|
1042
1205
|
std::time::SystemTime::now()
|
|
1043
1206
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -1117,14 +1280,15 @@ impl AdsProvider {
|
|
|
1117
1280
|
return Ok(Some(key.to_owned()));
|
|
1118
1281
|
}
|
|
1119
1282
|
let query = format!("identifier:\"{key}\"");
|
|
1120
|
-
let response
|
|
1283
|
+
let response = self
|
|
1121
1284
|
.client
|
|
1122
1285
|
.get("https://api.adsabs.harvard.edu/v1/search/query")
|
|
1123
1286
|
.headers(self.headers()?)
|
|
1124
1287
|
.query(&[("q", query.as_str()), ("fl", "bibcode"), ("rows", "1")])
|
|
1125
|
-
.send()
|
|
1126
|
-
|
|
1127
|
-
|
|
1288
|
+
.send();
|
|
1289
|
+
let response = provider_request(self.name(), key, response)?;
|
|
1290
|
+
let response = provider_request(self.name(), key, response.error_for_status())?;
|
|
1291
|
+
let response: AdsSearchResponse = provider_request(self.name(), key, response.json())?;
|
|
1128
1292
|
Ok(response
|
|
1129
1293
|
.response
|
|
1130
1294
|
.docs
|
|
@@ -1133,18 +1297,25 @@ impl AdsProvider {
|
|
|
1133
1297
|
}
|
|
1134
1298
|
|
|
1135
1299
|
fn export_bibtex_many(&self, bibcodes: &[String]) -> Result<BTreeMap<String, String>> {
|
|
1136
|
-
let
|
|
1300
|
+
let key = if bibcodes.is_empty() {
|
|
1301
|
+
"empty ADS export batch".to_owned()
|
|
1302
|
+
} else {
|
|
1303
|
+
bibcodes.join(", ")
|
|
1304
|
+
};
|
|
1305
|
+
let response = self
|
|
1137
1306
|
.client
|
|
1138
1307
|
.post("https://api.adsabs.harvard.edu/v1/export/bibtex")
|
|
1139
1308
|
.headers(self.headers()?)
|
|
1140
1309
|
.json(&json!({ "bibcode": bibcodes }))
|
|
1141
|
-
.send()
|
|
1142
|
-
|
|
1143
|
-
|
|
1310
|
+
.send();
|
|
1311
|
+
let response = provider_request(self.name(), key.clone(), response)?;
|
|
1312
|
+
let response = provider_request(self.name(), key.clone(), response.error_for_status())?;
|
|
1313
|
+
let response: AdsExportResponse = provider_request(self.name(), key, response.json())?;
|
|
1144
1314
|
let Some(export) = nonempty(&response.export) else {
|
|
1145
1315
|
return Ok(BTreeMap::new());
|
|
1146
1316
|
};
|
|
1147
1317
|
Ok(split_bib_entries(&export)
|
|
1318
|
+
.unwrap_or_default()
|
|
1148
1319
|
.into_iter()
|
|
1149
1320
|
.filter_map(|entry| {
|
|
1150
1321
|
let parsed = BibEntry::parse(entry)?;
|
|
@@ -1266,15 +1437,16 @@ impl BibliographyProvider for InspireProvider {
|
|
|
1266
1437
|
} else {
|
|
1267
1438
|
return Ok(None);
|
|
1268
1439
|
};
|
|
1269
|
-
let
|
|
1440
|
+
let response = self
|
|
1270
1441
|
.client
|
|
1271
1442
|
.get("https://inspirehep.net/api/literature")
|
|
1272
1443
|
.header(USER_AGENT, "bibsync/0.1")
|
|
1273
1444
|
.header(ACCEPT, "application/x-bibtex")
|
|
1274
1445
|
.query(&[("q", query.as_str()), ("format", "bibtex"), ("size", "1")])
|
|
1275
|
-
.send()
|
|
1276
|
-
|
|
1277
|
-
|
|
1446
|
+
.send();
|
|
1447
|
+
let response = provider_request(self.name(), key, response)?;
|
|
1448
|
+
let response = provider_request(self.name(), key, response.error_for_status())?;
|
|
1449
|
+
let bibtex = provider_request(self.name(), key, response.text())?;
|
|
1278
1450
|
let Some(bibtex) = nonempty(&bibtex) else {
|
|
1279
1451
|
return Ok(None);
|
|
1280
1452
|
};
|
|
@@ -1305,7 +1477,12 @@ impl BibliographyProvider for InspireProvider {
|
|
|
1305
1477
|
return Ok(BTreeMap::new());
|
|
1306
1478
|
}
|
|
1307
1479
|
|
|
1308
|
-
let
|
|
1480
|
+
let batch_key = if keys.is_empty() {
|
|
1481
|
+
"empty InspireHEP batch".to_owned()
|
|
1482
|
+
} else {
|
|
1483
|
+
keys.join(", ")
|
|
1484
|
+
};
|
|
1485
|
+
let response = self
|
|
1309
1486
|
.client
|
|
1310
1487
|
.get("https://inspirehep.net/api/literature")
|
|
1311
1488
|
.header(USER_AGENT, "bibsync/0.1")
|
|
@@ -1313,9 +1490,12 @@ impl BibliographyProvider for InspireProvider {
|
|
|
1313
1490
|
("q", query_parts.join(" OR ").as_str()),
|
|
1314
1491
|
("size", keys.len().to_string().as_str()),
|
|
1315
1492
|
])
|
|
1316
|
-
.send()
|
|
1317
|
-
|
|
1318
|
-
|
|
1493
|
+
.send();
|
|
1494
|
+
let response = provider_request(self.name(), batch_key.clone(), response)?;
|
|
1495
|
+
let response =
|
|
1496
|
+
provider_request(self.name(), batch_key.clone(), response.error_for_status())?;
|
|
1497
|
+
let response: InspireSearchResponse =
|
|
1498
|
+
provider_request(self.name(), batch_key, response.json())?;
|
|
1319
1499
|
|
|
1320
1500
|
let mut record_by_key = BTreeMap::new();
|
|
1321
1501
|
for hit in response.hits.hits {
|
|
@@ -1334,15 +1514,16 @@ impl BibliographyProvider for InspireProvider {
|
|
|
1334
1514
|
|
|
1335
1515
|
let mut resolved = BTreeMap::new();
|
|
1336
1516
|
for (key, record_id) in record_by_key {
|
|
1337
|
-
let
|
|
1517
|
+
let response = self
|
|
1338
1518
|
.client
|
|
1339
1519
|
.get(format!("https://inspirehep.net/api/literature/{record_id}"))
|
|
1340
1520
|
.header(USER_AGENT, "bibsync/0.1")
|
|
1341
1521
|
.header(ACCEPT, "application/x-bibtex")
|
|
1342
1522
|
.query(&[("format", "bibtex")])
|
|
1343
|
-
.send()
|
|
1344
|
-
|
|
1345
|
-
|
|
1523
|
+
.send();
|
|
1524
|
+
let response = provider_request(self.name(), key.clone(), response)?;
|
|
1525
|
+
let response = provider_request(self.name(), key.clone(), response.error_for_status())?;
|
|
1526
|
+
let bibtex = provider_request(self.name(), key.clone(), response.text())?;
|
|
1346
1527
|
let Some(bibtex) = nonempty(&bibtex) else {
|
|
1347
1528
|
continue;
|
|
1348
1529
|
};
|
|
@@ -1437,8 +1618,8 @@ pub fn pre_commit_hook_manifest(ignore_file: Option<&Path>) -> String {
|
|
|
1437
1618
|
#[cfg(test)]
|
|
1438
1619
|
mod tests {
|
|
1439
1620
|
use super::{
|
|
1440
|
-
BibliographyProvider, ProviderChoice, ResolvedEntry, SyncOptions,
|
|
1441
|
-
sync_files_with_provider,
|
|
1621
|
+
BibliographyProvider, ProviderChoice, ResolvedEntry, SyncOptions, UnresolvedCitation,
|
|
1622
|
+
citation_keys, sync_files_with_provider,
|
|
1442
1623
|
};
|
|
1443
1624
|
use std::cell::Cell;
|
|
1444
1625
|
use std::collections::BTreeMap;
|
|
@@ -1623,4 +1804,192 @@ mod tests {
|
|
|
1623
1804
|
assert_eq!(second.len(), 1);
|
|
1624
1805
|
assert_eq!(calls.get(), 1);
|
|
1625
1806
|
}
|
|
1807
|
+
|
|
1808
|
+
#[test]
|
|
1809
|
+
fn single_bib_update_requires_existing_input() {
|
|
1810
|
+
let dir = tempdir().expect("tempdir");
|
|
1811
|
+
let missing = dir.path().join("missing.bib");
|
|
1812
|
+
let provider = FakeProvider {
|
|
1813
|
+
entries: BTreeMap::new(),
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
let error = sync_files_with_provider(
|
|
1817
|
+
std::slice::from_ref(&missing),
|
|
1818
|
+
&SyncOptions::default(),
|
|
1819
|
+
&provider,
|
|
1820
|
+
)
|
|
1821
|
+
.expect_err("missing single bibliography should fail");
|
|
1822
|
+
|
|
1823
|
+
assert!(error.to_string().contains("bibliography input not found"));
|
|
1824
|
+
assert!(error.to_string().contains(&missing.display().to_string()));
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
#[test]
|
|
1828
|
+
fn other_bibliography_requires_existing_input() {
|
|
1829
|
+
let dir = tempdir().expect("tempdir");
|
|
1830
|
+
let tex = dir.path().join("main.tex");
|
|
1831
|
+
let bib = dir.path().join("refs.bib");
|
|
1832
|
+
let other = dir.path().join("shared.bib");
|
|
1833
|
+
std::fs::write(&tex, "\\cite{2404.14498}").expect("write tex");
|
|
1834
|
+
let provider = FakeProvider {
|
|
1835
|
+
entries: BTreeMap::new(),
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
let error = sync_files_with_provider(
|
|
1839
|
+
&[tex],
|
|
1840
|
+
&SyncOptions {
|
|
1841
|
+
output: Some(bib),
|
|
1842
|
+
other_bibliographies: vec![other.clone()],
|
|
1843
|
+
..SyncOptions::default()
|
|
1844
|
+
},
|
|
1845
|
+
&provider,
|
|
1846
|
+
)
|
|
1847
|
+
.expect_err("missing other bibliography should fail");
|
|
1848
|
+
|
|
1849
|
+
assert!(
|
|
1850
|
+
error
|
|
1851
|
+
.to_string()
|
|
1852
|
+
.contains("read-only bibliography not found")
|
|
1853
|
+
);
|
|
1854
|
+
assert!(error.to_string().contains(&other.display().to_string()));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
#[test]
|
|
1858
|
+
fn ignore_file_requires_existing_input() {
|
|
1859
|
+
let dir = tempdir().expect("tempdir");
|
|
1860
|
+
let tex = dir.path().join("main.tex");
|
|
1861
|
+
let bib = dir.path().join("refs.bib");
|
|
1862
|
+
let ignore = dir.path().join(".bibsyncignore");
|
|
1863
|
+
std::fs::write(&tex, "\\cite{NotAnIdentifier}").expect("write tex");
|
|
1864
|
+
let provider = FakeProvider {
|
|
1865
|
+
entries: BTreeMap::new(),
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1868
|
+
let error = sync_files_with_provider(
|
|
1869
|
+
&[tex],
|
|
1870
|
+
&SyncOptions {
|
|
1871
|
+
output: Some(bib),
|
|
1872
|
+
ignore_file: Some(ignore.clone()),
|
|
1873
|
+
..SyncOptions::default()
|
|
1874
|
+
},
|
|
1875
|
+
&provider,
|
|
1876
|
+
)
|
|
1877
|
+
.expect_err("missing ignore file should fail");
|
|
1878
|
+
|
|
1879
|
+
assert!(error.to_string().contains("ignore file not found"));
|
|
1880
|
+
assert!(error.to_string().contains(&ignore.display().to_string()));
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
#[test]
|
|
1884
|
+
fn malformed_bibtex_reports_file_and_parse_error() {
|
|
1885
|
+
let dir = tempdir().expect("tempdir");
|
|
1886
|
+
let bib = dir.path().join("refs.bib");
|
|
1887
|
+
std::fs::write(&bib, "@article{broken,\n title = {Missing close}\n")
|
|
1888
|
+
.expect("write malformed bib");
|
|
1889
|
+
let provider = FakeProvider {
|
|
1890
|
+
entries: BTreeMap::new(),
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
let error = sync_files_with_provider(
|
|
1894
|
+
std::slice::from_ref(&bib),
|
|
1895
|
+
&SyncOptions::default(),
|
|
1896
|
+
&provider,
|
|
1897
|
+
)
|
|
1898
|
+
.expect_err("malformed BibTeX should fail");
|
|
1899
|
+
|
|
1900
|
+
assert!(error.to_string().contains("invalid BibTeX"));
|
|
1901
|
+
assert!(error.to_string().contains("missing a closing"));
|
|
1902
|
+
assert!(error.to_string().contains(&bib.display().to_string()));
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
#[test]
|
|
1906
|
+
fn malformed_output_bibtex_reports_file_and_parse_error() {
|
|
1907
|
+
let dir = tempdir().expect("tempdir");
|
|
1908
|
+
let tex = dir.path().join("main.tex");
|
|
1909
|
+
let bib = dir.path().join("refs.bib");
|
|
1910
|
+
std::fs::write(&tex, "\\cite{NotAnIdentifier}").expect("write tex");
|
|
1911
|
+
std::fs::write(&bib, "@article{broken,\n title = {Missing close}\n")
|
|
1912
|
+
.expect("write malformed bib");
|
|
1913
|
+
let provider = FakeProvider {
|
|
1914
|
+
entries: BTreeMap::new(),
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
let error = sync_files_with_provider(
|
|
1918
|
+
&[tex],
|
|
1919
|
+
&SyncOptions {
|
|
1920
|
+
output: Some(bib.clone()),
|
|
1921
|
+
..SyncOptions::default()
|
|
1922
|
+
},
|
|
1923
|
+
&provider,
|
|
1924
|
+
)
|
|
1925
|
+
.expect_err("malformed output BibTeX should fail");
|
|
1926
|
+
|
|
1927
|
+
assert!(error.to_string().contains("invalid BibTeX"));
|
|
1928
|
+
assert!(error.to_string().contains("missing a closing"));
|
|
1929
|
+
assert!(error.to_string().contains(&bib.display().to_string()));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
#[test]
|
|
1933
|
+
fn unresolved_details_distinguish_unsupported_and_provider_miss() {
|
|
1934
|
+
let dir = tempdir().expect("tempdir");
|
|
1935
|
+
let tex = dir.path().join("main.tex");
|
|
1936
|
+
let bib = dir.path().join("refs.bib");
|
|
1937
|
+
std::fs::write(&tex, "\\cite{NotAnIdentifier,2404.14498}").expect("write tex");
|
|
1938
|
+
let provider = FakeProvider {
|
|
1939
|
+
entries: BTreeMap::new(),
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
let report = sync_files_with_provider(
|
|
1943
|
+
&[tex],
|
|
1944
|
+
&SyncOptions {
|
|
1945
|
+
output: Some(bib),
|
|
1946
|
+
..SyncOptions::default()
|
|
1947
|
+
},
|
|
1948
|
+
&provider,
|
|
1949
|
+
)
|
|
1950
|
+
.expect("sync");
|
|
1951
|
+
|
|
1952
|
+
assert_eq!(report.unresolved, vec!["2404.14498", "NotAnIdentifier"]);
|
|
1953
|
+
assert_eq!(
|
|
1954
|
+
report.unresolved_details,
|
|
1955
|
+
vec![
|
|
1956
|
+
UnresolvedCitation {
|
|
1957
|
+
key: "2404.14498".to_owned(),
|
|
1958
|
+
reason: super::UnresolvedReason::ProviderNoMatch,
|
|
1959
|
+
},
|
|
1960
|
+
UnresolvedCitation {
|
|
1961
|
+
key: "NotAnIdentifier".to_owned(),
|
|
1962
|
+
reason: super::UnresolvedReason::UnsupportedIdentifier,
|
|
1963
|
+
},
|
|
1964
|
+
]
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
#[test]
|
|
1969
|
+
fn corrupt_cache_json_reports_cache_path() {
|
|
1970
|
+
let dir = tempdir().expect("tempdir");
|
|
1971
|
+
let mapping = super::mapping_path(dir.path(), "inspire", "arxiv", "2404.14498");
|
|
1972
|
+
std::fs::create_dir_all(mapping.parent().expect("mapping parent"))
|
|
1973
|
+
.expect("create cache dir");
|
|
1974
|
+
std::fs::write(&mapping, "{").expect("write corrupt cache");
|
|
1975
|
+
let provider = CountingProvider {
|
|
1976
|
+
calls: Rc::new(Cell::new(0)),
|
|
1977
|
+
entries: BTreeMap::new(),
|
|
1978
|
+
};
|
|
1979
|
+
let cached = super::CachedProvider {
|
|
1980
|
+
inner: Box::new(provider),
|
|
1981
|
+
config: super::CacheConfig {
|
|
1982
|
+
enabled: true,
|
|
1983
|
+
refresh: false,
|
|
1984
|
+
root: dir.path().to_owned(),
|
|
1985
|
+
},
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
let error = cached
|
|
1989
|
+
.resolve_many(&["2404.14498".to_owned()])
|
|
1990
|
+
.expect_err("corrupt cache should fail");
|
|
1991
|
+
|
|
1992
|
+
assert!(error.to_string().contains("invalid JSON cache file"));
|
|
1993
|
+
assert!(error.to_string().contains(&mapping.display().to_string()));
|
|
1994
|
+
}
|
|
1626
1995
|
}
|
|
@@ -94,6 +94,17 @@ fn sync_files_py<'py>(
|
|
|
94
94
|
dict.set_item("existing", report.existing)?;
|
|
95
95
|
dict.set_item("found_in_other", report.found_in_other)?;
|
|
96
96
|
dict.set_item("unresolved", report.unresolved)?;
|
|
97
|
+
let unresolved_details = report
|
|
98
|
+
.unresolved_details
|
|
99
|
+
.into_iter()
|
|
100
|
+
.map(|detail| {
|
|
101
|
+
let item = PyDict::new(py);
|
|
102
|
+
item.set_item("key", detail.key)?;
|
|
103
|
+
item.set_item("reason", detail.reason.to_string())?;
|
|
104
|
+
Ok(item)
|
|
105
|
+
})
|
|
106
|
+
.collect::<PyResult<Vec<_>>>()?;
|
|
107
|
+
dict.set_item("unresolved_details", unresolved_details)?;
|
|
97
108
|
dict.set_item("changed", report.changed)?;
|
|
98
109
|
dict.set_item("check_mode", report.check_mode)?;
|
|
99
110
|
Ok(dict)
|
|
@@ -43,7 +43,47 @@ fn cli_defaults_to_check_mode() {
|
|
|
43
43
|
.arg(&tex)
|
|
44
44
|
.assert()
|
|
45
45
|
.failure()
|
|
46
|
-
.stdout(predicate::str::contains("unresolved:
|
|
46
|
+
.stdout(predicate::str::contains("unresolved:"))
|
|
47
|
+
.stdout(predicate::str::contains(
|
|
48
|
+
"NotAnIdentifier: unsupported identifier format",
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn cli_reports_missing_ignore_file() {
|
|
54
|
+
let dir = tempdir().expect("tempdir");
|
|
55
|
+
let tex = dir.path().join("main.tex");
|
|
56
|
+
let bib = dir.path().join("refs.bib");
|
|
57
|
+
let ignore = dir.path().join(".bibsyncignore");
|
|
58
|
+
std::fs::write(&tex, "\\cite{NotAnIdentifier}").expect("write tex");
|
|
59
|
+
|
|
60
|
+
let mut command = Command::cargo_bin("bibsync").expect("binary exists");
|
|
61
|
+
command
|
|
62
|
+
.arg("--output")
|
|
63
|
+
.arg(&bib)
|
|
64
|
+
.arg("--ignore-file")
|
|
65
|
+
.arg(&ignore)
|
|
66
|
+
.arg(&tex)
|
|
67
|
+
.assert()
|
|
68
|
+
.failure()
|
|
69
|
+
.stderr(predicate::str::contains("ignore file not found"))
|
|
70
|
+
.stderr(predicate::str::contains(ignore.display().to_string()));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn cli_reports_malformed_bibtex() {
|
|
75
|
+
let dir = tempdir().expect("tempdir");
|
|
76
|
+
let bib = dir.path().join("refs.bib");
|
|
77
|
+
std::fs::write(&bib, "@article{broken,\n title = {Missing close}\n")
|
|
78
|
+
.expect("write malformed bib");
|
|
79
|
+
|
|
80
|
+
let mut command = Command::cargo_bin("bibsync").expect("binary exists");
|
|
81
|
+
command
|
|
82
|
+
.arg(&bib)
|
|
83
|
+
.assert()
|
|
84
|
+
.failure()
|
|
85
|
+
.stderr(predicate::str::contains("invalid BibTeX"))
|
|
86
|
+
.stderr(predicate::str::contains("missing a closing"));
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
#[test]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|