calkit-python 0.48.1__py3-none-any.whl → 0.48.2__py3-none-any.whl

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.
Files changed (31) hide show
  1. calkit/cli/latex.py +454 -0
  2. calkit/git.py +4 -2
  3. calkit/latex.py +260 -10
  4. calkit/markdown.py +156 -5
  5. calkit/models/pipeline.py +104 -1
  6. calkit/pipeline.py +4 -1
  7. calkit/resources/devcontainer/devcontainer.json +1 -1
  8. calkit/resources/vscode/settings.json +1 -1
  9. calkit/tests/cli/main/test_core.py +15 -0
  10. calkit/tests/cli/test_latex.py +239 -0
  11. calkit/tests/test_docx.py +37 -3
  12. calkit/tests/test_git.py +29 -0
  13. calkit/tests/test_markdown.py +88 -0
  14. {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/METADATA +2 -1
  15. {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/RECORD +31 -31
  16. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  17. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  18. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  19. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  20. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
  21. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
  22. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
  23. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
  24. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
  25. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
  26. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
  27. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  28. {calkit_python-0.48.1.data → calkit_python-0.48.2.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  29. {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/WHEEL +0 -0
  30. {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/entry_points.txt +0 -0
  31. {calkit_python-0.48.1.dist-info → calkit_python-0.48.2.dist-info}/licenses/LICENSE +0 -0
calkit/cli/latex.py CHANGED
@@ -10,6 +10,7 @@ import shutil
10
10
  import string
11
11
  import subprocess
12
12
  import sys
13
+ from collections.abc import Callable
13
14
  from copy import deepcopy
14
15
  from pathlib import Path
15
16
 
@@ -189,6 +190,186 @@ def from_questions(
189
190
  json2latex.dump(command_name, values, f)
190
191
 
191
192
 
193
+ @latex_app.command(name="from-markdown")
194
+ def from_markdown(
195
+ md_path: Annotated[
196
+ str, typer.Argument(help="The Markdown file to build.")
197
+ ],
198
+ outputs: Annotated[
199
+ list[str],
200
+ typer.Option(
201
+ "--output",
202
+ "-o",
203
+ help="Where to write the PDF, or the LaTeX with a .tex "
204
+ "extension; can be given more than once.",
205
+ ),
206
+ ],
207
+ environment: Annotated[
208
+ str | None,
209
+ typer.Option(
210
+ "--environment",
211
+ "-e",
212
+ help="Environment to run pandoc and LaTeX in, e.g., one using "
213
+ "Calkit's LaTeX image, which includes both.",
214
+ ),
215
+ ] = None,
216
+ template_path: Annotated[
217
+ str | None, typer.Option("--template", help="Pandoc template.")
218
+ ] = None,
219
+ filters: Annotated[
220
+ list[str] | None,
221
+ typer.Option(
222
+ "--filter", help="Pandoc Lua filter; can be given more than once."
223
+ ),
224
+ ] = None,
225
+ pandoc_args: Annotated[
226
+ list[str] | None,
227
+ typer.Option(
228
+ "--pandoc-arg",
229
+ help="Another argument for pandoc; can be given more than once.",
230
+ ),
231
+ ] = None,
232
+ no_check: Annotated[
233
+ bool,
234
+ typer.Option("--no-check", help="Don't check the environment first."),
235
+ ] = False,
236
+ verbose: Annotated[
237
+ bool, typer.Option("--verbose", "-v", help="Print commands.")
238
+ ] = False,
239
+ ) -> None:
240
+ """Build a Markdown file into a PDF with pandoc and LaTeX.
241
+
242
+ Value markers are filled from their results files, which keeps the
243
+ Markdown itself current too, and each value links to the question on
244
+ the project's hub whose evidence cites it. The template gets the Calkit
245
+ version and the project's URL as the ``calkit-version``,
246
+ ``project-url``, and ``project`` variables.
247
+ """
248
+ import calkit.docx
249
+ import calkit.markdown
250
+
251
+ def question_url(value: calkit.markdown.MarkdownValue) -> str | None:
252
+ # The question whose evidence cites this value, or failing that,
253
+ # the first to cite its file
254
+ if project_url is None:
255
+ return None
256
+ first = None
257
+ for n, q in enumerate(ck_info.get("questions") or [], start=1):
258
+ evidence = q.get("evidence") if isinstance(q, dict) else None
259
+ for ev in evidence or []:
260
+ if not isinstance(ev, dict) or ev.get("path") != value.path:
261
+ continue
262
+ keys = [ev.get("key"), *(ev.get("values") or {}).values()]
263
+ if value.key in keys:
264
+ return f"{project_url}/questions/{n}"
265
+ first = first or n
266
+ return f"{project_url}/questions/{first}" if first else None
267
+
268
+ def to_latex(tex_path: str) -> None:
269
+ # Images are pointed at from where the LaTeX is, by a filter that
270
+ # runs before the others, so they see the paths LaTeX will
271
+ build_dir = os.path.dirname(tex_path) or "."
272
+ os.makedirs(build_dir, exist_ok=True)
273
+ stem = Path(tex_path).stem
274
+ src_path = f"{build_dir}/{stem}.pandoc.md"
275
+ with open(src_path, "w", encoding="utf-8", newline="\n") as f:
276
+ f.write(
277
+ calkit.markdown.prepare_for_pdf(
278
+ text, md_path, link=question_url
279
+ )
280
+ )
281
+ prefix = Path(os.path.relpath(md_dir, build_dir)).as_posix()
282
+ repoint = f"{build_dir}/{stem}.images.lua"
283
+ with open(repoint, "w", encoding="utf-8", newline="\n") as f:
284
+ f.write(calkit.markdown.image_prefix_filter(prefix))
285
+ args = [src_path, "-o", tex_path, "--standalone"]
286
+ if template_path is not None:
287
+ args += ["--template", template_path]
288
+ for path in [repoint, *(filters or [])]:
289
+ args += ["--lua-filter", path]
290
+ args += [f"--variable=calkit-version:{version}"]
291
+ if project_url is not None:
292
+ args += [
293
+ f"--variable=project-url:{project_url}",
294
+ f"--variable=project:{project_url.split('://', 1)[1]}",
295
+ ]
296
+ args += pandoc_args or []
297
+ if environment is not None:
298
+ cmd = _tex_cmd(
299
+ ["pandoc", *args], environment, no_check, verbose, dep="pandoc"
300
+ )
301
+ else:
302
+ pandoc = calkit.docx.find_pandoc()
303
+ if pandoc is None:
304
+ raise_error(
305
+ "Pandoc is needed; name an environment that has it, "
306
+ "e.g., one using Calkit's LaTeX image, or install it"
307
+ )
308
+ cmd = [pandoc, *args]
309
+ try:
310
+ subprocess.check_call(cmd)
311
+ except subprocess.CalledProcessError:
312
+ raise_error(f"Pandoc failed to convert {md_path}")
313
+
314
+ for output in outputs:
315
+ if not output.endswith((".pdf", ".tex")):
316
+ raise_error("Outputs must be .pdf or .tex files")
317
+ # Paths go into the build directory's, so they have to be the
318
+ # project's own
319
+ rel = os.path.relpath(md_path)
320
+ if rel == ".." or rel.startswith(".." + os.sep) or os.path.isabs(rel):
321
+ raise_error(f"{md_path} isn't in this project")
322
+ md_path = Path(rel).as_posix()
323
+ md_dir = os.path.dirname(md_path) or "."
324
+ ck_info = calkit.load_calkit_info()
325
+ project_url = None
326
+ if ck_info.get("owner") and ck_info.get("name"):
327
+ hub = str(ck_info.get("hub") or "calkit.io").rstrip("/")
328
+ if "://" not in hub:
329
+ hub = "https://" + hub
330
+ project_url = f"{hub}/{ck_info['owner']}/{ck_info['name']}"
331
+ # A local version's commit is the project's own, which isn't Calkit's
332
+ version = calkit.__version__.split("+")[0]
333
+ with open(md_path, encoding="utf-8") as f:
334
+ text = f.read()
335
+ try:
336
+ text, changed = calkit.markdown.set_values(text, md_path)
337
+ except calkit.markdown.MarkdownParseError as e:
338
+ raise_error(str(e))
339
+ if changed:
340
+ with open(md_path, "w", encoding="utf-8", newline="\n") as f:
341
+ f.write(text)
342
+ for output in outputs:
343
+ if output.endswith(".tex"):
344
+ to_latex(output)
345
+ if not any(o.endswith(".pdf") for o in outputs):
346
+ return
347
+ # A PDF is built where Calkit keeps what it derives from the file, and
348
+ # dated by the file's last commit rather than the build directory's,
349
+ # which has none of its own
350
+ build_dir = Path(".calkit", "markdown", md_path, "pdf").as_posix()
351
+ tex_path = f"{build_dir}/{Path(md_path).stem}.tex"
352
+ to_latex(tex_path)
353
+ cmd = [sys.executable, "-m", "calkit", "latex", "build"]
354
+ if environment is not None:
355
+ cmd += ["-e", environment]
356
+ if no_check:
357
+ cmd += ["--no-check"]
358
+ env = os.environ.copy()
359
+ epoch = calkit.latex.get_source_date_epoch(md_path)
360
+ if epoch is not None:
361
+ env["SOURCE_DATE_EPOCH"] = epoch
362
+ try:
363
+ subprocess.check_call(cmd + [tex_path], env=env)
364
+ except subprocess.CalledProcessError:
365
+ raise_error(f"LaTeX failed to build {tex_path}")
366
+ for output in outputs:
367
+ if output.endswith(".pdf"):
368
+ if os.path.dirname(output):
369
+ os.makedirs(os.path.dirname(output), exist_ok=True)
370
+ shutil.copyfile(Path(tex_path).with_suffix(".pdf"), output)
371
+
372
+
192
373
  def _tex_cmd(
193
374
  tex_cmd: list[str],
194
375
  environment: str | None,
@@ -2773,6 +2954,12 @@ def merge_docx(
2773
2954
  and existing.resolved == tc.resolved
2774
2955
  ):
2775
2956
  continue
2957
+ # Word can't carry these, so they come from the source
2958
+ tc.id, tc.issue, tc.attrs = (
2959
+ existing.id,
2960
+ existing.issue,
2961
+ existing.attrs,
2962
+ )
2776
2963
  # Word knows neither emails nor what the source already
2777
2964
  # recorded, so carry those over for unchanged messages
2778
2965
  known = {(e.author, e.text): e for e in existing.entries}
@@ -2850,3 +3037,270 @@ def merge_docx(
2850
3037
  f"already there, {counts['pending']} pending, {counts['unplaced']} "
2851
3038
  f"unplaced); {added} comments added, {updated} updated"
2852
3039
  )
3040
+
3041
+
3042
+ comments_app = typer.Typer(no_args_is_help=True)
3043
+ latex_app.add_typer(
3044
+ comments_app,
3045
+ name="comments",
3046
+ help="Work with review comments in LaTeX source.",
3047
+ )
3048
+
3049
+ _ID_OPTION = typer.Option("--id", help="ID of the thread.")
3050
+ _LINE_OPTION = typer.Option(
3051
+ "--line", "-l", help="Number of any line in the thread."
3052
+ )
3053
+
3054
+
3055
+ def _new_entry(
3056
+ text: str, author: str | None, email: str | None
3057
+ ) -> calkit.latex.Entry:
3058
+ import datetime
3059
+
3060
+ import git
3061
+
3062
+ if author is None:
3063
+ try:
3064
+ author = git.Git().config("--get", "user.name").strip()
3065
+ email = email or git.Git().config("--get", "user.email").strip()
3066
+ except git.GitCommandError:
3067
+ raise_error("Set --author, or Git's user.name")
3068
+ date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
3069
+ return calkit.latex.Entry(author, text, email or None, date)
3070
+
3071
+
3072
+ def _edit_comment(
3073
+ tex_path: str,
3074
+ id: str | None,
3075
+ line: int | None,
3076
+ edit: Callable[[calkit.latex.TexComment], None],
3077
+ ) -> calkit.latex.TexComment:
3078
+ """Apply an edit to one thread and write the file back, giving the
3079
+ thread an ID if it had none."""
3080
+ if (id is None) == (line is None):
3081
+ raise_error("Specify one of --id or --line")
3082
+ if not os.path.isfile(tex_path):
3083
+ raise_error(f"{tex_path} does not exist")
3084
+ lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
3085
+ tc = calkit.latex.find_comment(lines, id=id, lineno=line)
3086
+ if tc is None:
3087
+ raise_error(f"No comment thread in {tex_path} at {id or line}")
3088
+ edit(tc)
3089
+ tc.id = tc.id or calkit.latex.new_comment_id()
3090
+ calkit.latex.write_comment(lines, tc)
3091
+ Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
3092
+ return tc
3093
+
3094
+
3095
+ @comments_app.command(name="list")
3096
+ def list_comments(
3097
+ tex_path: Annotated[
3098
+ str,
3099
+ typer.Argument(
3100
+ help="Document to list comments from, including files it inputs."
3101
+ ),
3102
+ ],
3103
+ unresolved: Annotated[
3104
+ bool,
3105
+ typer.Option("--unresolved", "-u", help="Only list open threads."),
3106
+ ] = False,
3107
+ as_json: Annotated[
3108
+ bool, typer.Option("--json", help="Output result as JSON.")
3109
+ ] = False,
3110
+ ) -> None:
3111
+ """List the comment threads in a LaTeX document."""
3112
+ if not os.path.isfile(tex_path):
3113
+ raise_error(f"{tex_path} does not exist")
3114
+ out = [
3115
+ c
3116
+ for c in calkit.latex.list_comments(tex_path)
3117
+ if not (unresolved and c["resolved"])
3118
+ ]
3119
+ if as_json:
3120
+ typer.echo(json.dumps(out, indent=2))
3121
+ return
3122
+ for c in out:
3123
+ status = " (resolved)" if c["resolved"] else ""
3124
+ typer.echo(f"{c['path']}:{c['line']} [{c['id'] or '-'}]{status}")
3125
+ if c["highlight"]:
3126
+ typer.echo(f' On "{c["highlight"]["text"]}"')
3127
+ for m in c["messages"]:
3128
+ typer.echo(f" {m['author']}: {m['text']}")
3129
+
3130
+
3131
+ @comments_app.command(name="locate")
3132
+ def locate_paragraph(
3133
+ tex_path: Annotated[
3134
+ str,
3135
+ typer.Argument(help="Document to search, including files it inputs."),
3136
+ ],
3137
+ text: Annotated[
3138
+ str | None,
3139
+ typer.Option(
3140
+ "--text", help="Rendered text, e.g., a line copied from the PDF."
3141
+ ),
3142
+ ] = None,
3143
+ path: Annotated[
3144
+ str | None,
3145
+ typer.Option("--path", help="File with the line to look up."),
3146
+ ] = None,
3147
+ line: Annotated[
3148
+ int | None,
3149
+ typer.Option("--line", "-l", help="Line to look up in --path."),
3150
+ ] = None,
3151
+ as_json: Annotated[
3152
+ bool, typer.Option("--json", help="Output result as JSON.")
3153
+ ] = False,
3154
+ ) -> None:
3155
+ """Find the paragraph some rendered text is in, or a source line is in.
3156
+
3157
+ This is how a place in a PDF built without SyncTeX, or by someone else,
3158
+ can be matched to the source.
3159
+ """
3160
+ if (text is None) == (line is None):
3161
+ raise_error("Specify one of --text or --path and --line")
3162
+ if not os.path.isfile(tex_path):
3163
+ raise_error(f"{tex_path} does not exist")
3164
+ found = calkit.latex.locate_block(tex_path, text, path, line)
3165
+ if found is None:
3166
+ raise_error("No matching paragraph found")
3167
+ out = {
3168
+ "path": found.path,
3169
+ "line": found.lineno,
3170
+ "end_line": found.lines[-1].lineno,
3171
+ "text": found.text or calkit.latex.display_text(found),
3172
+ }
3173
+ if as_json:
3174
+ typer.echo(json.dumps(out, indent=2))
3175
+ else:
3176
+ typer.echo(f"{out['path']}:{out['line']}")
3177
+
3178
+
3179
+ @comments_app.command(name="add")
3180
+ def add_comment(
3181
+ tex_path: Annotated[str, typer.Argument(help="File to comment in.")],
3182
+ line: Annotated[
3183
+ int,
3184
+ typer.Option(
3185
+ "--line",
3186
+ "-l",
3187
+ help="A line in, or just above, the paragraph to comment on.",
3188
+ ),
3189
+ ],
3190
+ text: Annotated[str, typer.Option("--text", "-m", help="The comment.")],
3191
+ highlight: Annotated[
3192
+ str | None,
3193
+ typer.Option(
3194
+ "--highlight", help="Rendered text in the paragraph it's about."
3195
+ ),
3196
+ ] = None,
3197
+ occ: Annotated[
3198
+ int,
3199
+ typer.Option(
3200
+ "--occ", help="Which occurrence of the highlight text, from 0."
3201
+ ),
3202
+ ] = 0,
3203
+ author: Annotated[
3204
+ str | None,
3205
+ typer.Option("--author", help="Defaults to Git's user.name."),
3206
+ ] = None,
3207
+ email: Annotated[
3208
+ str | None,
3209
+ typer.Option("--email", help="Defaults to Git's user.email."),
3210
+ ] = None,
3211
+ as_json: Annotated[
3212
+ bool, typer.Option("--json", help="Output the thread as JSON.")
3213
+ ] = False,
3214
+ ) -> None:
3215
+ """Start a comment thread above a paragraph, printing its ID."""
3216
+ if not os.path.isfile(tex_path):
3217
+ raise_error(f"{tex_path} does not exist")
3218
+ lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
3219
+ tc = calkit.latex.TexComment(
3220
+ [_new_entry(text, author, email)],
3221
+ highlight=highlight or None,
3222
+ highlight_occ=occ,
3223
+ id=calkit.latex.new_comment_id(),
3224
+ )
3225
+ try:
3226
+ calkit.latex.add_comment(lines, line, tc)
3227
+ except ValueError as e:
3228
+ raise_error(str(e))
3229
+ Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
3230
+ if as_json:
3231
+ blks = calkit.latex.file_blocks(tex_path, lines)
3232
+ typer.echo(
3233
+ json.dumps(
3234
+ calkit.latex.comment_to_dict(tex_path, tc, blks), indent=2
3235
+ )
3236
+ )
3237
+ else:
3238
+ typer.echo(tc.id)
3239
+
3240
+
3241
+ @comments_app.command(name="reply")
3242
+ def reply_to_comment(
3243
+ tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
3244
+ text: Annotated[str, typer.Option("--text", "-m", help="The reply.")],
3245
+ id: Annotated[str | None, _ID_OPTION] = None,
3246
+ line: Annotated[int | None, _LINE_OPTION] = None,
3247
+ author: Annotated[
3248
+ str | None,
3249
+ typer.Option("--author", help="Defaults to Git's user.name."),
3250
+ ] = None,
3251
+ email: Annotated[
3252
+ str | None,
3253
+ typer.Option("--email", help="Defaults to Git's user.email."),
3254
+ ] = None,
3255
+ ) -> None:
3256
+ """Reply to a comment thread."""
3257
+ entry = _new_entry(text, author, email)
3258
+ tc = _edit_comment(tex_path, id, line, lambda tc: tc.entries.append(entry))
3259
+ typer.echo(tc.id)
3260
+
3261
+
3262
+ @comments_app.command(name="resolve")
3263
+ def resolve_comment(
3264
+ tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
3265
+ id: Annotated[str | None, _ID_OPTION] = None,
3266
+ line: Annotated[int | None, _LINE_OPTION] = None,
3267
+ ) -> None:
3268
+ """Mark a comment thread resolved."""
3269
+
3270
+ def edit(tc: calkit.latex.TexComment) -> None:
3271
+ tc.resolved = True
3272
+
3273
+ typer.echo(_edit_comment(tex_path, id, line, edit).id)
3274
+
3275
+
3276
+ @comments_app.command(name="reopen")
3277
+ def reopen_comment(
3278
+ tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
3279
+ id: Annotated[str | None, _ID_OPTION] = None,
3280
+ line: Annotated[int | None, _LINE_OPTION] = None,
3281
+ ) -> None:
3282
+ """Mark a resolved comment thread open again."""
3283
+
3284
+ def edit(tc: calkit.latex.TexComment) -> None:
3285
+ tc.resolved = False
3286
+
3287
+ typer.echo(_edit_comment(tex_path, id, line, edit).id)
3288
+
3289
+
3290
+ @comments_app.command(name="delete")
3291
+ def delete_comment(
3292
+ tex_path: Annotated[str, typer.Argument(help="File the thread is in.")],
3293
+ id: Annotated[str | None, _ID_OPTION] = None,
3294
+ line: Annotated[int | None, _LINE_OPTION] = None,
3295
+ ) -> None:
3296
+ """Delete a comment thread and its replies."""
3297
+ if (id is None) == (line is None):
3298
+ raise_error("Specify one of --id or --line")
3299
+ if not os.path.isfile(tex_path):
3300
+ raise_error(f"{tex_path} does not exist")
3301
+ lines = Path(tex_path).read_text(encoding="utf-8").split("\n")
3302
+ tc = calkit.latex.find_comment(lines, id=id, lineno=line)
3303
+ if tc is None:
3304
+ raise_error(f"No comment thread in {tex_path} at {id or line}")
3305
+ del lines[tc.lineno - 1 : tc.lineno - 1 + tc.nlines]
3306
+ Path(tex_path).write_text("\n".join(lines), encoding="utf-8")
calkit/git.py CHANGED
@@ -131,7 +131,8 @@ def get_staged_files(
131
131
  repo = get_repo(path)
132
132
  cmd = ["--staged", "--name-only"]
133
133
  if path is not None:
134
- cmd.append(path)
134
+ # A staged deletion's path is gone, so Git would read it as a ref
135
+ cmd += ["--", path]
135
136
  diff = repo.git.diff(cmd)
136
137
  paths = diff.split("\n")
137
138
  return [p for p in paths if p]
@@ -166,7 +167,8 @@ def get_staged_files_with_status(
166
167
  repo = get_repo(path)
167
168
  cmd = ["--staged", "--name-status"]
168
169
  if path is not None:
169
- cmd.append(path)
170
+ # A staged deletion's path is gone, so Git would read it as a ref
171
+ cmd += ["--", path]
170
172
  diff = repo.git.diff(cmd)
171
173
  paths = diff.split("\n")
172
174
  res = []