pyDiffTools 0.1.58__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.58/pyDiffTools.egg-info → pydifftools-0.1.59}/PKG-INFO +12 -2
  2. {pydifftools-0.1.58 → pydifftools-0.1.59}/README.rst +11 -1
  3. {pydifftools-0.1.58 → pydifftools-0.1.59/pyDiffTools.egg-info}/PKG-INFO +12 -2
  4. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyDiffTools.egg-info/SOURCES.txt +2 -0
  5. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/command_line.py +2 -1
  6. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/git_aliases.py +2 -0
  7. pydifftools-0.1.59/pydifftools/git_reattach.py +145 -0
  8. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyproject.toml +1 -1
  9. pydifftools-0.1.59/tests/test_git_reattach.py +186 -0
  10. {pydifftools-0.1.58 → pydifftools-0.1.59}/LICENSE.md +0 -0
  11. {pydifftools-0.1.58 → pydifftools-0.1.59}/MANIFEST.in +0 -0
  12. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/_quarto.yml +0 -0
  13. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  14. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  15. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  16. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  17. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  18. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  19. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  20. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  21. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  22. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  23. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  24. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  25. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  26. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  27. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  28. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  29. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  30. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  31. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  32. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  33. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  34. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  35. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/example.qmd +0 -0
  36. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/example.tex +0 -0
  37. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/index.qmd +0 -0
  38. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  39. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  40. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  41. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  42. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/independent.qmd +0 -0
  43. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/index.qmd +0 -0
  44. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  45. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  46. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  47. {pydifftools-0.1.58 → pydifftools-0.1.59}/example_notebook/project1/test_include.qmd +0 -0
  48. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  49. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyDiffTools.egg-info/entry_points.txt +0 -0
  50. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyDiffTools.egg-info/requires.txt +0 -0
  51. {pydifftools-0.1.58 → pydifftools-0.1.59}/pyDiffTools.egg-info/top_level.txt +0 -0
  52. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/__init__.py +0 -0
  53. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/bibliography.py +0 -0
  54. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/browser_lifecycle.py +0 -0
  55. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/check_numbers.py +0 -0
  56. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/citation_dialog.py +0 -0
  57. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/command_registry.py +0 -0
  58. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_filter_history.json +0 -0
  59. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_functions.py +0 -0
  60. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_migration.py +0 -0
  61. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_tags.lua +0 -0
  62. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_tags_margin.lua +0 -0
  63. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_tags_no_comments.lua +0 -0
  64. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comment_toggle.js +0 -0
  65. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comments.css +0 -0
  66. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/comments_author_colors.css +0 -0
  67. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/completions/git-mergein +0 -0
  68. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/continuous.py +0 -0
  69. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/copy_files.py +0 -0
  70. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/diff-doc.js +0 -0
  71. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/doc_contents.py +0 -0
  72. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/__init__.py +0 -0
  73. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/comparison.py +0 -0
  74. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  75. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/graph.py +0 -0
  76. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/source_jump.js +0 -0
  77. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/svg-pan-zoom.min.js +0 -0
  78. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/watch_graph.py +0 -0
  79. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/flowchart/wgrph_view.js +0 -0
  80. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/forward_search.py +0 -0
  81. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/git_gd.py +0 -0
  82. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/git_gd_history.py +0 -0
  83. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/git_gd_image.py +0 -0
  84. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/git_gd_qt.py +0 -0
  85. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/html_comments.py +0 -0
  86. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/html_uncomments.py +0 -0
  87. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/line_alignment.py +0 -0
  88. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/log_example.py +0 -0
  89. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/match_spaces.py +0 -0
  90. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/notebook/__init__.py +0 -0
  91. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/notebook/display.py +0 -0
  92. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/notebook/fast_build.py +0 -0
  93. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/notebook/tex_to_qmd.py +0 -0
  94. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/onewordify.py +0 -0
  95. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/onewordify_undo.py +0 -0
  96. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/outline.py +0 -0
  97. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/pdf_diff.py +0 -0
  98. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/rearrange_tex.py +0 -0
  99. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/searchacro.py +0 -0
  100. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/separate_comments.py +0 -0
  101. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/source_jump.py +0 -0
  102. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/split_conflict.py +0 -0
  103. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/unseparate_comments.py +0 -0
  104. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/update_check.py +0 -0
  105. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/wrap_sentences.py +0 -0
  106. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/wrapping_options.py +0 -0
  107. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/xml2xlsx.vbs +0 -0
  108. {pydifftools-0.1.58 → pydifftools-0.1.59}/pydifftools/zotero.py +0 -0
  109. {pydifftools-0.1.58 → pydifftools-0.1.59}/setup.cfg +0 -0
  110. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_bibliography.py +0 -0
  111. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_browser_lifecycle.py +0 -0
  112. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_check_single_use.py +0 -0
  113. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_command_line_help.py +0 -0
  114. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_comment_filters.py +0 -0
  115. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_comment_migration.py +0 -0
  116. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_continuous_shutdown.py +0 -0
  117. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_forward_search.py +0 -0
  118. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_git_gd_history.py +0 -0
  119. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_git_gd_image.py +0 -0
  120. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_git_gd_qt.py +0 -0
  121. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_git_mergein.py +0 -0
  122. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_git_pdf_diff.py +0 -0
  123. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_line_alignment.py +0 -0
  124. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_pdf_diff.py +0 -0
  125. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_qt_lifecycle.py +0 -0
  126. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_rrng.py +0 -0
  127. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_source_jump.py +0 -0
  128. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_tex_to_qmd.py +0 -0
  129. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_update_check.py +0 -0
  130. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_wrap_check.py +0 -0
  131. {pydifftools-0.1.58 → pydifftools-0.1.59}/tests/test_wrapping.py +0 -0
  132. {pydifftools-0.1.58 → 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.58
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.58
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
@@ -36,6 +36,7 @@ from .comment_functions import matchingbrackets
36
36
  from .copy_files import copy_image_files
37
37
  from .searchacro import replace_acros
38
38
  from .rearrange_tex import run as rearrange_tex_run
39
+ from .git_reattach import reattach # registers branch reattachment
39
40
  from .git_gd import gd # registers git difftool review command
40
41
  from .pdf_diff import pd, git_pd # registers PDF diff command
41
42
  from .git_aliases import (
@@ -909,7 +910,7 @@ def build_parser():
909
910
  # argparse does not validate REMAINDER choices; main checks them first.
910
911
  subparsers.container.add_argument(
911
912
  "--add_to_git", nargs=argparse.REMAINDER, choices=list(GIT_ALIASES),
912
- 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 "
913
914
  "(without a subcommand)."),
914
915
  )
915
916
  for name, spec in _COMMAND_SPECS.items():
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.58"
7
+ version = "0.1.59"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -0,0 +1,186 @@
1
+ """Reattachment against real repositories, including remote-only branches."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import subprocess
6
+ import sys
7
+ import time
8
+
9
+ import pytest
10
+
11
+ from pydifftools.git_reattach import reattach
12
+ from pydifftools import command_line
13
+
14
+
15
+ def git(path, *args):
16
+ return subprocess.check_output(
17
+ ["git", "-C", str(path), *map(str, args)],
18
+ stderr=subprocess.STDOUT, text=True,
19
+ ).strip()
20
+
21
+
22
+ @pytest.fixture
23
+ def project(tmp_path, monkeypatch):
24
+ monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "global"))
25
+ monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
26
+ monkeypatch.setenv("GIT_AUTHOR_NAME", "Test")
27
+ monkeypatch.setenv("GIT_AUTHOR_EMAIL", "test@example.invalid")
28
+ monkeypatch.setenv("GIT_COMMITTER_NAME", "Test")
29
+ monkeypatch.setenv("GIT_COMMITTER_EMAIL", "test@example.invalid")
30
+ remote = tmp_path / "remote.git"
31
+ seed = tmp_path / "seed"
32
+ work = tmp_path / "work space"
33
+ git(tmp_path, "init", "--bare", "--initial-branch=main", remote)
34
+ git(tmp_path, "clone", remote, seed)
35
+ (seed / "file").write_text("main\n")
36
+ git(seed, "add", "file")
37
+ git(seed, "commit", "-m", "main")
38
+ git(seed, "checkout", "-b", "work/topic")
39
+ (seed / "file").write_text("topic\n")
40
+ git(seed, "commit", "-am", "topic")
41
+ git(seed, "push", "origin", "main", "work/topic")
42
+ git(tmp_path, "clone", remote, work)
43
+ git(work, "checkout", "--detach", "origin/work/topic")
44
+ return seed, work
45
+
46
+
47
+ def test_remote_only_match_tracks_and_preserves_files(project):
48
+ _, work = project
49
+ before = git(work, "rev-parse", "HEAD")
50
+ (work / "file").write_text("uncommitted edit\n")
51
+ (work / "untracked").write_text("keep\n")
52
+ reattach(str(work))
53
+ assert git(work, "branch", "--show-current") == "work/topic"
54
+ assert git(work, "rev-parse", "--abbrev-ref", "@{upstream}") == (
55
+ "origin/work/topic"
56
+ )
57
+ assert git(work, "rev-parse", "HEAD") == before
58
+ assert (work / "file").read_text() == "uncommitted edit\n"
59
+ assert (work / "untracked").read_text() == "keep\n"
60
+ reattach(str(work))
61
+ assert git(work, "rev-parse", "HEAD") == before
62
+
63
+
64
+ def test_local_match_and_dry_run(project):
65
+ _, work = project
66
+ git(work, "branch", "topic-local", "HEAD")
67
+ reattach(str(work), dry_run=True)
68
+ assert git(work, "branch", "--show-current") == ""
69
+ reattach(str(work))
70
+ assert git(work, "branch", "--show-current") == "topic-local"
71
+
72
+
73
+ def test_origin_default_disambiguates_local_branches(project):
74
+ _, work = project
75
+ git(work, "checkout", "--detach", "origin/main")
76
+ git(work, "branch", "alternative", "HEAD")
77
+ reattach(str(work))
78
+ assert git(work, "branch", "--show-current") == "main"
79
+
80
+
81
+ def test_default_remote_branch_wins_over_other_remote_matches(project):
82
+ _, work = project
83
+ git(work, "checkout", "--detach", "origin/main")
84
+ git(work, "branch", "-D", "main")
85
+ git(work, "update-ref", "refs/remotes/origin/alternative", "HEAD")
86
+ reattach(str(work))
87
+ assert git(work, "branch", "--show-current") == "main"
88
+ assert git(work, "rev-parse", "--abbrev-ref", "@{u}") == "origin/main"
89
+
90
+
91
+ def test_ambiguous_remote_matches_are_not_chosen_arbitrarily(project, capsys):
92
+ _, work = project
93
+ git(work, "update-ref", "refs/remotes/origin/also-topic", "HEAD")
94
+ reattach(str(work))
95
+ assert git(work, "branch", "--show-current") == ""
96
+ assert "ambiguous" in capsys.readouterr().out
97
+
98
+
99
+ def test_existing_local_branch_is_never_reset(project, capsys):
100
+ _, work = project
101
+ git(work, "branch", "work/topic", "origin/main")
102
+ before = git(work, "rev-parse", "work/topic")
103
+ reattach(str(work))
104
+ assert git(work, "branch", "--show-current") == ""
105
+ assert git(work, "rev-parse", "work/topic") == before
106
+ assert "points elsewhere" in capsys.readouterr().out
107
+
108
+
109
+ def test_no_match_stays_at_recorded_commit(project):
110
+ _, work = project
111
+ (work / "file").write_text("unpublished\n")
112
+ git(work, "commit", "-am", "unpublished")
113
+ before = git(work, "rev-parse", "HEAD")
114
+ reattach(str(work))
115
+ assert git(work, "branch", "--show-current") == ""
116
+ assert git(work, "rev-parse", "HEAD") == before
117
+
118
+
119
+ def test_non_origin_remote_and_branch_with_slash(project):
120
+ _, work = project
121
+ git(work, "remote", "rename", "origin", "upstream")
122
+ reattach(str(work))
123
+ assert git(work, "rev-parse", "--abbrev-ref", "@{u}") == (
124
+ "upstream/work/topic"
125
+ )
126
+
127
+
128
+ def test_recursive_submodule_and_cli_registration(project, tmp_path):
129
+ seed, work = project
130
+ parent = tmp_path / "parent"
131
+ git(tmp_path, "init", "--initial-branch=main", parent)
132
+ git(parent, "-c", "protocol.file.allow=always", "submodule", "add",
133
+ str(seed), "child with spaces")
134
+ child = parent / "child with spaces"
135
+ git(child, "checkout", "--detach", "origin/work/topic")
136
+ git(child, "branch", "-D", "work/topic")
137
+ git(parent, "commit", "-am", "child")
138
+ args = command_line.build_parser().parse_args(
139
+ ["reattach", "--path", str(parent)]
140
+ )
141
+ args._handler(path=args.path, no_recursive=args.no_recursive,
142
+ fetch=args.fetch, dry_run=args.dry_run)
143
+ assert git(child, "branch", "--show-current") == "work/topic"
144
+ assert git(parent, "branch", "--show-current") == "main"
145
+ assert git(child, "rev-parse", "HEAD") == git(work, "rev-parse", "HEAD")
146
+
147
+
148
+ def test_checked_out_branch_elsewhere_is_not_stolen(project, tmp_path):
149
+ _, work = project
150
+ git(work, "branch", "topic-local", "HEAD")
151
+ git(work, "worktree", "add", tmp_path / "other", "topic-local")
152
+ with pytest.raises(SystemExit):
153
+ reattach(str(work))
154
+ assert git(work, "branch", "--show-current") == ""
155
+
156
+
157
+ def test_fetch_finds_remote_only_match(project):
158
+ _, work = project
159
+ git(work, "update-ref", "-d", "refs/remotes/origin/work/topic")
160
+ reattach(str(work), fetch=True)
161
+ assert git(work, "branch", "--show-current") == "work/topic"
162
+ assert git(work, "rev-parse", "--abbrev-ref", "@{u}") == (
163
+ "origin/work/topic"
164
+ )
165
+
166
+
167
+ def test_installed_git_alias_forwards_options(project, tmp_path, monkeypatch):
168
+ _, work = project
169
+ launcher = tmp_path / "pydifft"
170
+ launcher.write_text(
171
+ f"#!{sys.executable}\n"
172
+ "from pydifftools.command_line import main\nmain()\n"
173
+ )
174
+ launcher.chmod(0o755)
175
+ monkeypatch.setenv("PATH", str(tmp_path) + os.pathsep + os.environ["PATH"])
176
+ monkeypatch.setenv("PYTHONPATH", str(Path(__file__).resolve().parents[1]))
177
+ monkeypatch.setenv(
178
+ "PYDIFFTOOLS_UPDATE_CHECK_LAST_RAN_UTC_DATE",
179
+ time.strftime("%Y-%m-%d", time.gmtime()),
180
+ )
181
+ monkeypatch.chdir(work)
182
+ command_line.main(["--add_to_git", "reattach"])
183
+ git(work, "reattach", "--dry-run", "--no-recursive")
184
+ assert git(work, "branch", "--show-current") == ""
185
+ git(work, "reattach", "--no-recursive")
186
+ assert git(work, "branch", "--show-current") == "work/topic"
File without changes
File without changes
File without changes