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.
Files changed (132) hide show
  1. {pydifftools-0.1.57/pyDiffTools.egg-info → pydifftools-0.1.59}/PKG-INFO +12 -2
  2. {pydifftools-0.1.57 → pydifftools-0.1.59}/README.rst +11 -1
  3. {pydifftools-0.1.57 → pydifftools-0.1.59/pyDiffTools.egg-info}/PKG-INFO +12 -2
  4. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/SOURCES.txt +2 -0
  5. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/browser_lifecycle.py +92 -12
  6. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/command_line.py +13 -22
  7. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/continuous.py +31 -46
  8. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_aliases.py +2 -0
  9. pydifftools-0.1.59/pydifftools/git_reattach.py +145 -0
  10. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/wrap_sentences.py +11 -2
  11. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyproject.toml +1 -1
  12. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_browser_lifecycle.py +82 -0
  13. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_continuous_shutdown.py +89 -72
  14. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_forward_search.py +1 -1
  15. pydifftools-0.1.59/tests/test_git_reattach.py +186 -0
  16. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_wrap_check.py +40 -0
  17. {pydifftools-0.1.57 → pydifftools-0.1.59}/LICENSE.md +0 -0
  18. {pydifftools-0.1.57 → pydifftools-0.1.59}/MANIFEST.in +0 -0
  19. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/_quarto.yml +0 -0
  20. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  21. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  22. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  23. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  24. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  25. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  26. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  27. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  28. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  29. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  30. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  31. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  32. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  33. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  34. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  35. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  36. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  37. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  38. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  39. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  40. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  41. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  42. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/example.qmd +0 -0
  43. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/example.tex +0 -0
  44. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/index.qmd +0 -0
  45. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  46. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  47. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  48. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  49. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/independent.qmd +0 -0
  50. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/index.qmd +0 -0
  51. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  52. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  53. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  54. {pydifftools-0.1.57 → pydifftools-0.1.59}/example_notebook/project1/test_include.qmd +0 -0
  55. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  56. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/entry_points.txt +0 -0
  57. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/requires.txt +0 -0
  58. {pydifftools-0.1.57 → pydifftools-0.1.59}/pyDiffTools.egg-info/top_level.txt +0 -0
  59. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/__init__.py +0 -0
  60. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/bibliography.py +0 -0
  61. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/check_numbers.py +0 -0
  62. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/citation_dialog.py +0 -0
  63. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/command_registry.py +0 -0
  64. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_filter_history.json +0 -0
  65. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_functions.py +0 -0
  66. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_migration.py +0 -0
  67. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags.lua +0 -0
  68. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags_margin.lua +0 -0
  69. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_tags_no_comments.lua +0 -0
  70. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comment_toggle.js +0 -0
  71. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comments.css +0 -0
  72. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/comments_author_colors.css +0 -0
  73. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/completions/git-mergein +0 -0
  74. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/copy_files.py +0 -0
  75. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/diff-doc.js +0 -0
  76. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/doc_contents.py +0 -0
  77. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/__init__.py +0 -0
  78. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/comparison.py +0 -0
  79. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  80. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/graph.py +0 -0
  81. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/source_jump.js +0 -0
  82. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  83. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/watch_graph.py +0 -0
  84. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/flowchart/wgrph_view.js +0 -0
  85. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/forward_search.py +0 -0
  86. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd.py +0 -0
  87. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_history.py +0 -0
  88. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_image.py +0 -0
  89. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/git_gd_qt.py +0 -0
  90. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/html_comments.py +0 -0
  91. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/html_uncomments.py +0 -0
  92. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/line_alignment.py +0 -0
  93. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/log_example.py +0 -0
  94. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/match_spaces.py +0 -0
  95. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/__init__.py +0 -0
  96. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/display.py +0 -0
  97. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/fast_build.py +0 -0
  98. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/notebook/tex_to_qmd.py +0 -0
  99. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/onewordify.py +0 -0
  100. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/onewordify_undo.py +0 -0
  101. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/outline.py +0 -0
  102. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/pdf_diff.py +0 -0
  103. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/rearrange_tex.py +0 -0
  104. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/searchacro.py +0 -0
  105. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/separate_comments.py +0 -0
  106. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/source_jump.py +0 -0
  107. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/split_conflict.py +0 -0
  108. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/unseparate_comments.py +0 -0
  109. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/update_check.py +0 -0
  110. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/wrapping_options.py +0 -0
  111. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/xml2xlsx.vbs +0 -0
  112. {pydifftools-0.1.57 → pydifftools-0.1.59}/pydifftools/zotero.py +0 -0
  113. {pydifftools-0.1.57 → pydifftools-0.1.59}/setup.cfg +0 -0
  114. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_bibliography.py +0 -0
  115. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_check_single_use.py +0 -0
  116. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_command_line_help.py +0 -0
  117. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_comment_filters.py +0 -0
  118. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_comment_migration.py +0 -0
  119. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_history.py +0 -0
  120. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_image.py +0 -0
  121. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_gd_qt.py +0 -0
  122. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_mergein.py +0 -0
  123. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_git_pdf_diff.py +0 -0
  124. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_line_alignment.py +0 -0
  125. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_pdf_diff.py +0 -0
  126. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_qt_lifecycle.py +0 -0
  127. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_rrng.py +0 -0
  128. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_source_jump.py +0 -0
  129. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_tex_to_qmd.py +0 -0
  130. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_update_check.py +0 -0
  131. {pydifftools-0.1.57 → pydifftools-0.1.59}/tests/test_wrapping.py +0 -0
  132. {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.57
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.57
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 that run before any dialog needs the user; cpb registers one
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
- """Let watch commands close their preview before a dialog appears."""
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 Exception:
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
- var searchText = arguments[0];
142
- if (!window.find) {
143
- return false;
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
- var selection = window.getSelection();
227
+ const selection = window.getSelection();
148
228
  if (selection.rangeCount > 0) {
149
- var rect = selection.getRangeAt(0).getBoundingClientRect();
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
- marker_match = re.search(r"(?i)@[a-z]{2,4}:", search_text)
559
- if marker_match:
560
- search_text = search_text[: marker_match.start()]
561
- search_text = re.sub(r"\[@[^\]]+\]", " ", search_text)
562
- # Strip paired markup delimiters (bold/italic, strikethrough,
563
- # superscript/subscript). These delimiters hug the text they wrap with no
564
- # space in the rendered output (e.g. "x^2^" renders as "x2"), so they are
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
- if search_text in fp.read():
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
- raise RuntimeError(
1343
- "The cpb forward-search listener stopped unexpectedly; "
1344
- "closing the preview instead of leaving an "
1345
- "undiscoverable session."
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
- forward_search_in_browser(chrome, search_text)
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 chrome is not None:
1454
- if not browser_window_is_alive(chrome):
1455
- break
1456
- try:
1457
- chrome.refresh()
1458
- except WebDriverException:
1459
- print(
1460
- "pydifft cpb: Browser is no longer "
1461
- "available; stopping without "
1462
- "reopening it.",
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
- dialog_callbacks.clear()
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
- for paragraph in PARAGRAPH_SPLIT.split(text):
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(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.57"
7
+ version = "0.1.59"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"