pyDiffTools 0.1.57__tar.gz → 0.1.59__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.
- {pydifftools-0.1.57/pyDiffTools.egg-info → pydifftools-0.1.59}/PKG-INFO +12 -2
- {pydifftools-0.1.57 → pydifftools-0.1.59}/README.rst +11 -1
- {pydifftools-0.1.57 → pydifftools-0.1.59/pyDiffTools.egg-info}/PKG-INFO +12 -2
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/SOURCES.txt +2 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/browser_lifecycle.py +92 -12
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/command_line.py +13 -22
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/continuous.py +31 -46
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_aliases.py +2 -0
- pydifftools-0.1.59/pydifftools/git_reattach.py +145 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/wrap_sentences.py +11 -2
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyproject.toml +1 -1
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_browser_lifecycle.py +82 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_continuous_shutdown.py +89 -72
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_forward_search.py +1 -1
- pydifftools-0.1.59/tests/test_git_reattach.py +186 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_wrap_check.py +40 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/LICENSE.md +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/MANIFEST.in +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/bibliography.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/citation_dialog.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_filter_history.json +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_migration.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags_margin.lua +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags_no_comments.lua +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comments_author_colors.css +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/completions/git-mergein +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/comparison.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/graph.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/source_jump.js +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/watch_graph.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/wgrph_view.js +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/forward_search.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_history.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_image.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_qt.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/line_alignment.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/log_example.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/display.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/pdf_diff.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/source_jump.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/wrapping_options.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/zotero.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/setup.cfg +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_bibliography.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_check_single_use.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_command_line_help.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_comment_filters.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_comment_migration.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_history.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_image.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_qt.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_mergein.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_pdf_diff.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_line_alignment.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_pdf_diff.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_qt_lifecycle.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_source_jump.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_update_check.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_wrapping.py +0 -0
- {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_zotero.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.59
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -274,10 +274,20 @@ included are (listed in order of fun/utility):
|
|
|
274
274
|
retaining the exact command of an existing global ``gd`` alias. If none
|
|
275
275
|
is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
|
|
276
276
|
Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
|
|
277
|
-
- ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
|
|
277
|
+
- ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
|
|
278
278
|
of these global Git aliases in one invocation. Installation appears
|
|
279
279
|
alongside the subcommands in root help; the former per-command
|
|
280
280
|
``--install`` and ``--add-to-git`` flags have been removed.
|
|
281
|
+
- ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
|
|
282
|
+
branches pointing at exactly their current commits, including initialized
|
|
283
|
+
submodules recursively. Remote-only matches create local tracking branches.
|
|
284
|
+
It never resets existing branches or pulls newer commits. With several
|
|
285
|
+
matches it prefers the origin default branch when possible, otherwise it
|
|
286
|
+
reports ambiguity and leaves the checkout detached. A local branch with
|
|
287
|
+
the required name pointing elsewhere is preserved. ``--dry-run`` previews
|
|
288
|
+
changes, ``--no-recursive`` limits the operation to one repository, and
|
|
289
|
+
``--fetch`` refreshes remote refs before matching. Install the global
|
|
290
|
+
``git reattach`` alias with ``pydifft --add_to_git reattach``.
|
|
281
291
|
- ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
|
|
282
292
|
branch without switching branches, then merges it into the current
|
|
283
293
|
branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
|
|
@@ -219,10 +219,20 @@ included are (listed in order of fun/utility):
|
|
|
219
219
|
retaining the exact command of an existing global ``gd`` alias. If none
|
|
220
220
|
is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
|
|
221
221
|
Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
|
|
222
|
-
- ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
|
|
222
|
+
- ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
|
|
223
223
|
of these global Git aliases in one invocation. Installation appears
|
|
224
224
|
alongside the subcommands in root help; the former per-command
|
|
225
225
|
``--install`` and ``--add-to-git`` flags have been removed.
|
|
226
|
+
- ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
|
|
227
|
+
branches pointing at exactly their current commits, including initialized
|
|
228
|
+
submodules recursively. Remote-only matches create local tracking branches.
|
|
229
|
+
It never resets existing branches or pulls newer commits. With several
|
|
230
|
+
matches it prefers the origin default branch when possible, otherwise it
|
|
231
|
+
reports ambiguity and leaves the checkout detached. A local branch with
|
|
232
|
+
the required name pointing elsewhere is preserved. ``--dry-run`` previews
|
|
233
|
+
changes, ``--no-recursive`` limits the operation to one repository, and
|
|
234
|
+
``--fetch`` refreshes remote refs before matching. Install the global
|
|
235
|
+
``git reattach`` alias with ``pydifft --add_to_git reattach``.
|
|
226
236
|
- ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
|
|
227
237
|
branch without switching branches, then merges it into the current
|
|
228
238
|
branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyDiffTools
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.59
|
|
4
4
|
Summary: Diff tools
|
|
5
5
|
Author: J M Franck
|
|
6
6
|
License: Copyright (c) 2015, jmfranck
|
|
@@ -274,10 +274,20 @@ included are (listed in order of fun/utility):
|
|
|
274
274
|
retaining the exact command of an existing global ``gd`` alias. If none
|
|
275
275
|
is configured, it installs ``!f() { pydifft gd "$@"; }; f``.
|
|
276
276
|
Configure ``difftool.mygvim.cmd`` for the GUI diff tool.
|
|
277
|
-
- ``pydifft --add_to_git gd pd tree mergein`` installs any selected subset
|
|
277
|
+
- ``pydifft --add_to_git gd pd tree mergein reattach`` installs any selected subset
|
|
278
278
|
of these global Git aliases in one invocation. Installation appears
|
|
279
279
|
alongside the subcommands in root help; the former per-command
|
|
280
280
|
``--install`` and ``--add-to-git`` flags have been removed.
|
|
281
|
+
- ``pydifft reattach [--path DIRECTORY]`` attaches detached checkouts to
|
|
282
|
+
branches pointing at exactly their current commits, including initialized
|
|
283
|
+
submodules recursively. Remote-only matches create local tracking branches.
|
|
284
|
+
It never resets existing branches or pulls newer commits. With several
|
|
285
|
+
matches it prefers the origin default branch when possible, otherwise it
|
|
286
|
+
reports ambiguity and leaves the checkout detached. A local branch with
|
|
287
|
+
the required name pointing elsewhere is preserved. ``--dry-run`` previews
|
|
288
|
+
changes, ``--no-recursive`` limits the operation to one repository, and
|
|
289
|
+
``--fetch`` refreshes remote refs before matching. Install the global
|
|
290
|
+
``git reattach`` alias with ``pydifft --add_to_git reattach``.
|
|
281
291
|
- ``pydifft mergein BRANCH [--remote REMOTE]`` updates an incoming local
|
|
282
292
|
branch without switching branches, then merges it into the current
|
|
283
293
|
branch. Install ``git mergein`` with ``pydifft --add_to_git mergein``.
|
|
@@ -70,6 +70,7 @@ pydifftools/git_gd.py
|
|
|
70
70
|
pydifftools/git_gd_history.py
|
|
71
71
|
pydifftools/git_gd_image.py
|
|
72
72
|
pydifftools/git_gd_qt.py
|
|
73
|
+
pydifftools/git_reattach.py
|
|
73
74
|
pydifftools/html_comments.py
|
|
74
75
|
pydifftools/html_uncomments.py
|
|
75
76
|
pydifftools/line_alignment.py
|
|
@@ -116,6 +117,7 @@ tests/test_git_gd_image.py
|
|
|
116
117
|
tests/test_git_gd_qt.py
|
|
117
118
|
tests/test_git_mergein.py
|
|
118
119
|
tests/test_git_pdf_diff.py
|
|
120
|
+
tests/test_git_reattach.py
|
|
119
121
|
tests/test_line_alignment.py
|
|
120
122
|
tests/test_pdf_diff.py
|
|
121
123
|
tests/test_qt_lifecycle.py
|
|
@@ -4,8 +4,7 @@ import shutil
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
# callbacks
|
|
8
|
-
# that closes its live preview, so issues are dealt with before it reopens
|
|
7
|
+
# Optional callbacks for callers that need to prepare for a dialog.
|
|
9
8
|
dialog_callbacks = []
|
|
10
9
|
|
|
11
10
|
|
|
@@ -97,7 +96,7 @@ def start_browser():
|
|
|
97
96
|
|
|
98
97
|
|
|
99
98
|
def prepare_for_dialog():
|
|
100
|
-
"""
|
|
99
|
+
"""Notify registered callers before a dialog appears."""
|
|
101
100
|
for callback in list(dialog_callbacks):
|
|
102
101
|
callback()
|
|
103
102
|
|
|
@@ -108,6 +107,10 @@ def browser_window_is_alive(browser):
|
|
|
108
107
|
# the same shutdown behavior when a user closes the browser window.
|
|
109
108
|
# Do not probe with execute_script here: page navigations can briefly
|
|
110
109
|
# interrupt script execution even while the window is still open.
|
|
110
|
+
from selenium.common.exceptions import (
|
|
111
|
+
InvalidSessionIdException, NoSuchWindowException, WebDriverException,
|
|
112
|
+
)
|
|
113
|
+
|
|
111
114
|
if browser is None:
|
|
112
115
|
return False
|
|
113
116
|
try:
|
|
@@ -115,10 +118,14 @@ def browser_window_is_alive(browser):
|
|
|
115
118
|
if not handles:
|
|
116
119
|
return False
|
|
117
120
|
return True
|
|
118
|
-
except
|
|
121
|
+
except (InvalidSessionIdException, NoSuchWindowException):
|
|
119
122
|
return False
|
|
123
|
+
except WebDriverException:
|
|
124
|
+
# A transient command failure is not evidence of a closed window.
|
|
125
|
+
return True
|
|
120
126
|
|
|
121
127
|
|
|
128
|
+
# also used by: continuous.py, notebook/fast_build.py, flowchart/watch_graph.py
|
|
122
129
|
def close_browser_window(browser):
|
|
123
130
|
# Close a Selenium browser session and ignore errors from already-closed
|
|
124
131
|
# windows so cleanup paths stay simple.
|
|
@@ -137,20 +144,93 @@ def forward_search_in_browser(browser, search_text):
|
|
|
137
144
|
if browser is None or not search_text:
|
|
138
145
|
return False
|
|
139
146
|
found = browser.execute_script(
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
r"""
|
|
148
|
+
const searchText = arguments[0];
|
|
149
|
+
let didFind = window.find && window.find(searchText);
|
|
150
|
+
// Match across inline elements, entities, and rendered references.
|
|
151
|
+
if (!didFind) {
|
|
152
|
+
const escape = text => text.replace(
|
|
153
|
+
/[.*+?^${}()|[\]\\]/g, '\\$&'
|
|
154
|
+
).replace(/\s+/g, '\\s+');
|
|
155
|
+
const reference =
|
|
156
|
+
/\[?@([a-z]{2,4}):(\w(?:[\w:./-]*[\w/-])?)\]?/gi;
|
|
157
|
+
let pattern = '', cursor = 0, match;
|
|
158
|
+
let valid = true;
|
|
159
|
+
while ((match = reference.exec(searchText))) {
|
|
160
|
+
pattern += escape(searchText.slice(cursor, match.index));
|
|
161
|
+
const id = match[1].toLowerCase() + ':' + match[2];
|
|
162
|
+
const link = Array.from(
|
|
163
|
+
document.querySelectorAll('a[href]')
|
|
164
|
+
).find(a => a.getAttribute('href') === '#' + id);
|
|
165
|
+
const target = document.getElementById(id);
|
|
166
|
+
if (!link && !target) {
|
|
167
|
+
valid = false;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// Unlinked crossrefs still render a numeric label. For a
|
|
171
|
+
// reference alone, select its target rather than guessing
|
|
172
|
+
// which occurrence of that number the user intended.
|
|
173
|
+
if (!link && searchText.trim() === match[0]) {
|
|
174
|
+
const range = document.createRange();
|
|
175
|
+
range.selectNodeContents(target);
|
|
176
|
+
const selection = window.getSelection();
|
|
177
|
+
selection.removeAllRanges();
|
|
178
|
+
selection.addRange(range);
|
|
179
|
+
didFind = true;
|
|
180
|
+
valid = false;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
pattern += '(?:(?:eq(?:uation)?|fig(?:ure)?|tab(?:le)?|' +
|
|
184
|
+
'sec(?:tion)?|ch(?:apter)?|lst|listing)s?\\.?\\s*)?' +
|
|
185
|
+
(link ? escape(link.textContent.trim()) :
|
|
186
|
+
'\\(?\\d+(?:[.–-]\\d+)*[a-z]?\\)?');
|
|
187
|
+
cursor = reference.lastIndex;
|
|
188
|
+
}
|
|
189
|
+
pattern += escape(searchText.slice(cursor));
|
|
190
|
+
if (valid) {
|
|
191
|
+
const walker = document.createTreeWalker(
|
|
192
|
+
document.body, NodeFilter.SHOW_TEXT
|
|
193
|
+
);
|
|
194
|
+
const nodes = [], starts = [];
|
|
195
|
+
let text = '', node;
|
|
196
|
+
while ((node = walker.nextNode())) {
|
|
197
|
+
if (node.parentElement.closest(
|
|
198
|
+
'script, style, noscript, [hidden]'
|
|
199
|
+
) || !node.parentElement.getClientRects().length) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
starts.push(text.length);
|
|
203
|
+
nodes.push(node);
|
|
204
|
+
text += node.textContent;
|
|
205
|
+
}
|
|
206
|
+
const result = new RegExp(pattern, 'i').exec(text);
|
|
207
|
+
if (result && result[0].length) {
|
|
208
|
+
const start = result.index;
|
|
209
|
+
const end = start + result[0].length;
|
|
210
|
+
const first = starts.findIndex((offset, i) =>
|
|
211
|
+
offset + nodes[i].length > start
|
|
212
|
+
);
|
|
213
|
+
const last = starts.findIndex((offset, i) =>
|
|
214
|
+
offset + nodes[i].length >= end
|
|
215
|
+
);
|
|
216
|
+
const range = document.createRange();
|
|
217
|
+
range.setStart(nodes[first], start - starts[first]);
|
|
218
|
+
range.setEnd(nodes[last], end - starts[last]);
|
|
219
|
+
const selection = window.getSelection();
|
|
220
|
+
selection.removeAllRanges();
|
|
221
|
+
selection.addRange(range);
|
|
222
|
+
didFind = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
144
225
|
}
|
|
145
|
-
var didFind = window.find(searchText);
|
|
146
226
|
if (didFind && window.getSelection) {
|
|
147
|
-
|
|
227
|
+
const selection = window.getSelection();
|
|
148
228
|
if (selection.rangeCount > 0) {
|
|
149
|
-
|
|
229
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
150
230
|
window.scrollBy(0, rect.top - window.innerHeight / 3);
|
|
151
231
|
}
|
|
152
232
|
}
|
|
153
|
-
return didFind;
|
|
233
|
+
return Boolean(didFind);
|
|
154
234
|
""",
|
|
155
235
|
search_text,
|
|
156
236
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# PYTHON_ARGCOMPLETE_OK
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import html
|
|
4
5
|
import sys
|
|
5
6
|
import os
|
|
6
7
|
import gzip
|
|
@@ -35,6 +36,7 @@ from .comment_functions import matchingbrackets
|
|
|
35
36
|
from .copy_files import copy_image_files
|
|
36
37
|
from .searchacro import replace_acros
|
|
37
38
|
from .rearrange_tex import run as rearrange_tex_run
|
|
39
|
+
from .git_reattach import reattach # registers branch reattachment
|
|
38
40
|
from .git_gd import gd # registers git difftool review command
|
|
39
41
|
from .pdf_diff import pd, git_pd # registers PDF diff command
|
|
40
42
|
from .git_aliases import (
|
|
@@ -554,26 +556,14 @@ def fs(arguments):
|
|
|
554
556
|
)
|
|
555
557
|
def mfs(text):
|
|
556
558
|
# Normalize markdown-specific markup so search phrases match rendered text.
|
|
557
|
-
search_text = text
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# dropped rather than replaced with a space. Plain str.replace is used
|
|
566
|
-
# (not regex) so this is linear time regardless of how many delimiter
|
|
567
|
-
# characters appear.
|
|
568
|
-
search_text = (
|
|
569
|
-
search_text.replace("**", "")
|
|
570
|
-
.replace("__", "")
|
|
571
|
-
.replace("~~", "")
|
|
572
|
-
.replace("*", "")
|
|
573
|
-
.replace("_", "")
|
|
574
|
-
.replace("~", "")
|
|
575
|
-
.replace("^", "")
|
|
576
|
-
)
|
|
559
|
+
search_text = html.unescape(text)
|
|
560
|
+
search_text = re.sub(r"\[@(?![a-z]{2,4}:)[^\]]+\]", " ", search_text,
|
|
561
|
+
flags=re.IGNORECASE)
|
|
562
|
+
# Keep reference IDs intact while removing surrounding Markdown markup.
|
|
563
|
+
parts = re.split(r"(?i)(@[a-z]{2,4}:\w(?:[\w:./-]*[\w/-])?)", search_text)
|
|
564
|
+
for index in range(0, len(parts), 2):
|
|
565
|
+
parts[index] = re.sub(r"[*_~^]", "", parts[index])
|
|
566
|
+
search_text = "".join(parts)
|
|
577
567
|
search_text = re.sub(r"\s+", " ", search_text).strip()
|
|
578
568
|
if not search_text:
|
|
579
569
|
search_text = text.strip()
|
|
@@ -620,7 +610,8 @@ def mfs(text):
|
|
|
620
610
|
if not filename.endswith(".md"):
|
|
621
611
|
continue
|
|
622
612
|
with open(filename, encoding="utf-8") as fp:
|
|
623
|
-
|
|
613
|
+
source_text = fp.read()
|
|
614
|
+
if text.strip() in source_text or search_text in source_text:
|
|
624
615
|
matching_files.append(filename)
|
|
625
616
|
if len(matching_files) == 0:
|
|
626
617
|
raise RuntimeError(
|
|
@@ -919,7 +910,7 @@ def build_parser():
|
|
|
919
910
|
# argparse does not validate REMAINDER choices; main checks them first.
|
|
920
911
|
subparsers.container.add_argument(
|
|
921
912
|
"--add_to_git", nargs=argparse.REMAINDER, choices=list(GIT_ALIASES),
|
|
922
|
-
help=("Install one or more global Git aliases: gd, pd, tree, mergein "
|
|
913
|
+
help=("Install one or more global Git aliases: gd, pd, tree, mergein, reattach "
|
|
923
914
|
"(without a subcommand)."),
|
|
924
915
|
)
|
|
925
916
|
for name, spec in _COMMAND_SPECS.items():
|
|
@@ -11,6 +11,7 @@ import re
|
|
|
11
11
|
import shutil
|
|
12
12
|
import threading
|
|
13
13
|
import queue
|
|
14
|
+
import signal
|
|
14
15
|
import traceback
|
|
15
16
|
import tempfile
|
|
16
17
|
from pathlib import Path
|
|
@@ -27,7 +28,6 @@ from .comment_migration import prepare_comment_source
|
|
|
27
28
|
from .browser_lifecycle import (
|
|
28
29
|
browser_window_is_alive,
|
|
29
30
|
close_browser_window,
|
|
30
|
-
dialog_callbacks,
|
|
31
31
|
forward_search_in_browser,
|
|
32
32
|
prepare_for_dialog,
|
|
33
33
|
start_browser,
|
|
@@ -1231,11 +1231,16 @@ def cpb(
|
|
|
1231
1231
|
daemon=True,
|
|
1232
1232
|
)
|
|
1233
1233
|
chrome = None
|
|
1234
|
-
scroll_position = 0
|
|
1235
1234
|
observer = None
|
|
1236
1235
|
observer_started = False
|
|
1237
1236
|
socket_thread_started = False
|
|
1238
1237
|
source_jump_server = None
|
|
1238
|
+
previous_sigterm = None
|
|
1239
|
+
if threading.current_thread() is threading.main_thread():
|
|
1240
|
+
def stop_cpb(_signum, _frame):
|
|
1241
|
+
raise KeyboardInterrupt
|
|
1242
|
+
|
|
1243
|
+
previous_sigterm = signal.signal(signal.SIGTERM, stop_cpb)
|
|
1239
1244
|
|
|
1240
1245
|
try:
|
|
1241
1246
|
# Bind and serve before any build or browser work. This makes the fixed
|
|
@@ -1321,29 +1326,18 @@ def cpb(
|
|
|
1321
1326
|
# and never be reported as a change.
|
|
1322
1327
|
chrome.get("file://" + os.path.abspath(html_file))
|
|
1323
1328
|
|
|
1324
|
-
def close_for_dialog():
|
|
1325
|
-
# a rebuild needs the user, so close the preview while the
|
|
1326
|
-
# issues are dealt with; it reopens once the rebuild is done
|
|
1327
|
-
nonlocal chrome, scroll_position
|
|
1328
|
-
if chrome is not None:
|
|
1329
|
-
# sessionStorage does not survive quitting the browser.
|
|
1330
|
-
scroll_position = chrome.execute_script(
|
|
1331
|
-
"return window.scrollY;"
|
|
1332
|
-
)
|
|
1333
|
-
close_browser_window(chrome)
|
|
1334
|
-
chrome = None
|
|
1335
|
-
|
|
1336
|
-
dialog_callbacks.append(close_for_dialog)
|
|
1337
1329
|
rebuild_pending = False
|
|
1338
1330
|
stable_signature = None
|
|
1339
1331
|
stable_since = None
|
|
1332
|
+
listener_warning_shown = False
|
|
1340
1333
|
while True:
|
|
1341
|
-
if not socket_thread.is_alive():
|
|
1342
|
-
|
|
1343
|
-
"
|
|
1344
|
-
"
|
|
1345
|
-
|
|
1334
|
+
if not socket_thread.is_alive() and not listener_warning_shown:
|
|
1335
|
+
print(
|
|
1336
|
+
"pydifft cpb: forward-search listener stopped; "
|
|
1337
|
+
"forward search is unavailable. The preview remains open.",
|
|
1338
|
+
file=sys.stderr,
|
|
1346
1339
|
)
|
|
1340
|
+
listener_warning_shown = True
|
|
1347
1341
|
# Browser closure is terminal. Source-file and build failures do
|
|
1348
1342
|
# not affect browser ownership or observer lifetime.
|
|
1349
1343
|
if not browser_window_is_alive(chrome):
|
|
@@ -1352,7 +1346,11 @@ def cpb(
|
|
|
1352
1346
|
for queued_search in drain_forward_search_queue(search_queue):
|
|
1353
1347
|
search_text = queued_search.strip()
|
|
1354
1348
|
if search_text:
|
|
1355
|
-
|
|
1349
|
+
try:
|
|
1350
|
+
forward_search_in_browser(chrome, search_text)
|
|
1351
|
+
except WebDriverException as exc:
|
|
1352
|
+
print(f"pydifft cpb: forward search failed: {exc}",
|
|
1353
|
+
file=sys.stderr)
|
|
1356
1354
|
|
|
1357
1355
|
saw_source_event = False
|
|
1358
1356
|
while not change_queue.empty():
|
|
@@ -1450,30 +1448,16 @@ def cpb(
|
|
|
1450
1448
|
traceback.print_exc()
|
|
1451
1449
|
else:
|
|
1452
1450
|
handled_signature = attempted_signature
|
|
1453
|
-
if
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
file=sys.stderr,
|
|
1464
|
-
)
|
|
1465
|
-
break
|
|
1466
|
-
if chrome is None:
|
|
1467
|
-
# {{{ reopen the preview a dialog closed
|
|
1468
|
-
chrome = start_browser()
|
|
1469
|
-
chrome.get(
|
|
1470
|
-
"file://" + os.path.abspath(html_file)
|
|
1471
|
-
)
|
|
1472
|
-
chrome.execute_script(
|
|
1473
|
-
"window.scrollTo(0, arguments[0]);",
|
|
1474
|
-
scroll_position,
|
|
1475
|
-
)
|
|
1476
|
-
# }}}
|
|
1451
|
+
if not browser_window_is_alive(chrome):
|
|
1452
|
+
break
|
|
1453
|
+
try:
|
|
1454
|
+
chrome.refresh()
|
|
1455
|
+
except WebDriverException as exc:
|
|
1456
|
+
print(
|
|
1457
|
+
"pydifft cpb: preview refresh failed: "
|
|
1458
|
+
f"{exc}",
|
|
1459
|
+
file=sys.stderr,
|
|
1460
|
+
)
|
|
1477
1461
|
# }}}
|
|
1478
1462
|
time.sleep(MAIN_LOOP_INTERVAL_SECONDS)
|
|
1479
1463
|
except KeyboardInterrupt:
|
|
@@ -1489,7 +1473,8 @@ def cpb(
|
|
|
1489
1473
|
if socket_thread_started:
|
|
1490
1474
|
socket_thread.join()
|
|
1491
1475
|
close_browser_window(chrome)
|
|
1492
|
-
|
|
1476
|
+
if previous_sigterm is not None:
|
|
1477
|
+
signal.signal(signal.SIGTERM, previous_sigterm)
|
|
1493
1478
|
|
|
1494
1479
|
|
|
1495
1480
|
if __name__ == "__main__":
|
|
@@ -11,10 +11,12 @@ GIT_ALIASES = {
|
|
|
11
11
|
"gd": '!f() { pydifft gd "$@"; }; f',
|
|
12
12
|
"pd": '!f() { pydifft pd --git "$@"; }; f',
|
|
13
13
|
"tree": '!f() { pydifft tree "$@"; }; f',
|
|
14
|
+
"reattach": '!f() { pydifft reattach "$@"; }; f',
|
|
14
15
|
"mergein": '!f() { pydifft mergein "$@"; }; f',
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
# also used by: pydifftools/command_line.py for global CLI alias installation.
|
|
18
20
|
def install_git_alias(name, command, preserve_existing=False):
|
|
19
21
|
"""Install an alias and its completion, optionally retaining its value."""
|
|
20
22
|
if preserve_existing:
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Attach detached checkouts without changing the selected commits."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from .command_registry import register_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _git(path, *args, allowed=(0,)):
|
|
10
|
+
result = subprocess.run(
|
|
11
|
+
["git", "-C", str(path), *args], capture_output=True, text=True,
|
|
12
|
+
)
|
|
13
|
+
if result.returncode not in allowed:
|
|
14
|
+
raise RuntimeError(result.stderr.strip() or "Git command failed")
|
|
15
|
+
return result.stdout.rstrip("\n")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_command(
|
|
19
|
+
"reattach detached repositories to matching local or remote branches",
|
|
20
|
+
description=(
|
|
21
|
+
"Keep each checkout's current commit and attach a branch whose tip "
|
|
22
|
+
"matches it exactly. Includes initialized submodules recursively. "
|
|
23
|
+
"A remote-only match creates a local tracking branch. Existing "
|
|
24
|
+
"branches are never reset. Ambiguous or unmatched checkouts stay "
|
|
25
|
+
"detached. Cached remote branches are used unless --fetch is given."
|
|
26
|
+
),
|
|
27
|
+
help={
|
|
28
|
+
"path": "Repository or a directory inside it (default: current).",
|
|
29
|
+
"no_recursive": "Only inspect this repository, not its submodules.",
|
|
30
|
+
"fetch": "Fetch remote branches before looking for matches.",
|
|
31
|
+
"dry_run": "Show proposed switches without changing anything.",
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
def reattach(path=".", no_recursive=False, fetch=False, dry_run=False):
|
|
35
|
+
"""Find exact branch-tip matches, preserving commits and working files."""
|
|
36
|
+
if fetch and dry_run:
|
|
37
|
+
raise SystemExit("reattach: --fetch cannot be used with --dry-run")
|
|
38
|
+
try:
|
|
39
|
+
root = Path(_git(path, "rev-parse", "--show-toplevel"))
|
|
40
|
+
paths = [root]
|
|
41
|
+
if not no_recursive:
|
|
42
|
+
children = _git(
|
|
43
|
+
root, "submodule", "foreach", "--quiet", "--recursive",
|
|
44
|
+
'printf "%s\\0" "$PWD"',
|
|
45
|
+
)
|
|
46
|
+
paths.extend(Path(p) for p in children.split("\0") if p)
|
|
47
|
+
failed = False
|
|
48
|
+
for checkout in paths:
|
|
49
|
+
label = str(checkout)
|
|
50
|
+
try:
|
|
51
|
+
if _git(
|
|
52
|
+
checkout, "symbolic-ref", "--quiet", "HEAD",
|
|
53
|
+
allowed=(0, 1),
|
|
54
|
+
):
|
|
55
|
+
print(f"{label}: already on a branch; unchanged")
|
|
56
|
+
continue
|
|
57
|
+
if fetch:
|
|
58
|
+
_git(checkout, "fetch", "--all", "--prune",
|
|
59
|
+
"--no-recurse-submodules")
|
|
60
|
+
head = _git(checkout, "rev-parse", "HEAD")
|
|
61
|
+
# {{{ ask Git for exact matches, excluding symbolic refs
|
|
62
|
+
refs = _git(
|
|
63
|
+
checkout, "for-each-ref",
|
|
64
|
+
"--format=%(refname)%00%(symref)", "--points-at", head,
|
|
65
|
+
"refs/heads/", "refs/remotes/",
|
|
66
|
+
)
|
|
67
|
+
matches = [
|
|
68
|
+
line.split("\0")[0] for line in refs.splitlines()
|
|
69
|
+
if line.endswith("\0")
|
|
70
|
+
]
|
|
71
|
+
local = [r for r in matches if r.startswith("refs/heads/")]
|
|
72
|
+
remote = [r for r in matches
|
|
73
|
+
if r.startswith("refs/remotes/")]
|
|
74
|
+
default = _git(
|
|
75
|
+
checkout, "symbolic-ref", "--quiet",
|
|
76
|
+
"refs/remotes/origin/HEAD", allowed=(0, 1),
|
|
77
|
+
)
|
|
78
|
+
default_name = default.removeprefix("refs/remotes/origin/")
|
|
79
|
+
branch = None
|
|
80
|
+
upstream = None
|
|
81
|
+
if local:
|
|
82
|
+
preferred = "refs/heads/" + default_name
|
|
83
|
+
if preferred in local:
|
|
84
|
+
branch = default_name
|
|
85
|
+
elif len(local) == 1:
|
|
86
|
+
branch = local[0].removeprefix("refs/heads/")
|
|
87
|
+
elif remote:
|
|
88
|
+
# Derive names from configured remotes, retaining slashes
|
|
89
|
+
# in branch names and allowing slashes in remote names.
|
|
90
|
+
remotes = sorted(
|
|
91
|
+
_git(checkout, "remote").splitlines(),
|
|
92
|
+
key=len, reverse=True,
|
|
93
|
+
)
|
|
94
|
+
candidates = {}
|
|
95
|
+
for ref in remote:
|
|
96
|
+
for name in remotes:
|
|
97
|
+
prefix = f"refs/remotes/{name}/"
|
|
98
|
+
if ref.startswith(prefix):
|
|
99
|
+
candidates[ref] = ref[len(prefix):]
|
|
100
|
+
break
|
|
101
|
+
if default in candidates:
|
|
102
|
+
upstream = default
|
|
103
|
+
elif len(candidates) == 1:
|
|
104
|
+
upstream = next(iter(candidates))
|
|
105
|
+
elif len(set(candidates.values())) == 1:
|
|
106
|
+
name = next(iter(candidates.values()))
|
|
107
|
+
preferred = "refs/remotes/origin/" + name
|
|
108
|
+
if preferred in candidates:
|
|
109
|
+
upstream = preferred
|
|
110
|
+
if upstream:
|
|
111
|
+
branch = candidates[upstream]
|
|
112
|
+
existing = _git(
|
|
113
|
+
checkout, "show-ref", "--verify", "--hash",
|
|
114
|
+
f"refs/heads/{branch}", allowed=(0, 1, 128),
|
|
115
|
+
)
|
|
116
|
+
if existing:
|
|
117
|
+
print(
|
|
118
|
+
f"{label}: local {branch} points elsewhere; "
|
|
119
|
+
"left detached without resetting it"
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
# }}}
|
|
123
|
+
if branch is None:
|
|
124
|
+
reason = "ambiguous branch matches" if matches else (
|
|
125
|
+
"no branch tip matches this commit"
|
|
126
|
+
)
|
|
127
|
+
print(f"{label}: {reason}; left detached")
|
|
128
|
+
continue
|
|
129
|
+
action = "create tracking branch" if upstream else "switch to"
|
|
130
|
+
print(f"{label}: {action} {branch}")
|
|
131
|
+
if not dry_run:
|
|
132
|
+
if upstream:
|
|
133
|
+
_git(checkout, "switch", "--track", "-c", branch,
|
|
134
|
+
upstream)
|
|
135
|
+
else:
|
|
136
|
+
_git(checkout, "switch", "--no-guess", "--", branch)
|
|
137
|
+
if _git(checkout, "rev-parse", "HEAD") != head:
|
|
138
|
+
raise RuntimeError("branch moved during reattachment")
|
|
139
|
+
except RuntimeError as exc:
|
|
140
|
+
print(f"{label}: {exc}")
|
|
141
|
+
failed = True
|
|
142
|
+
if failed:
|
|
143
|
+
raise SystemExit(1)
|
|
144
|
+
except (OSError, RuntimeError) as exc:
|
|
145
|
+
raise SystemExit(f"reattach: {exc}") from None
|
|
@@ -703,7 +703,14 @@ def wrap_prose(
|
|
|
703
703
|
"""Wrap prose with sentence and punctuation rules for both commands."""
|
|
704
704
|
output = []
|
|
705
705
|
# Keep paragraph separators and the boundary newlines next to exclusions.
|
|
706
|
-
|
|
706
|
+
paragraphs = PARAGRAPH_SPLIT.split(text)
|
|
707
|
+
if filetype == "markdown":
|
|
708
|
+
# Hard line breaks are source boundaries, not wrappable whitespace.
|
|
709
|
+
paragraphs = [
|
|
710
|
+
part for paragraph in paragraphs
|
|
711
|
+
for part in re.split(r"( {2,}\r?\n)", paragraph)
|
|
712
|
+
]
|
|
713
|
+
for paragraph in paragraphs:
|
|
707
714
|
if not paragraph.strip():
|
|
708
715
|
output.append(paragraph)
|
|
709
716
|
continue
|
|
@@ -1001,7 +1008,9 @@ def check_prose(
|
|
|
1001
1008
|
if k < len(separators):
|
|
1002
1009
|
sep = separators[k]
|
|
1003
1010
|
offset += len(sep)
|
|
1004
|
-
if sep == " " and (
|
|
1011
|
+
if sep == " " and not re.match(
|
|
1012
|
+
r"[ \t]*\r?\n", stripped[offset:]
|
|
1013
|
+
) and (
|
|
1005
1014
|
k + 1 >= len(sentences) or sentences[k + 1].strip()
|
|
1006
1015
|
):
|
|
1007
1016
|
issue_line = para_line + stripped.count(
|