gitlatex 2.0.0__tar.gz → 2.1.0__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 (186) hide show
  1. {gitlatex-2.0.0/gitlatex.egg-info → gitlatex-2.1.0}/PKG-INFO +6 -2
  2. {gitlatex-2.0.0 → gitlatex-2.1.0}/README.md +5 -1
  3. gitlatex-2.1.0/docs/sync-and-conflicts.md +206 -0
  4. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/_version.py +3 -3
  5. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/http.py +13 -0
  6. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/__init__.py +3 -0
  7. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/compile.py +4 -1
  8. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/git.py +41 -9
  9. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/repos.py +4 -1
  10. gitlatex-2.1.0/gitlatex/routes/scm.py +221 -0
  11. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/workspace.py +7 -1
  12. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/comments.py +83 -1
  13. gitlatex-2.1.0/gitlatex/services/comments_merge.py +106 -0
  14. gitlatex-2.1.0/gitlatex/services/scm.py +406 -0
  15. gitlatex-2.1.0/gitlatex/services/sync.py +168 -0
  16. gitlatex-2.1.0/gitlatex/state.py +34 -0
  17. gitlatex-2.0.0/gitlatex/ui/assets/PdfViewer-Bd94D2QV.js → gitlatex-2.1.0/gitlatex/ui/assets/PdfViewer-CYPYydmG.js +2 -2
  18. gitlatex-2.1.0/gitlatex/ui/assets/index-Bd4rVZK8.css +1 -0
  19. gitlatex-2.1.0/gitlatex/ui/assets/index-DL-0rlAe.js +2179 -0
  20. gitlatex-2.0.0/gitlatex/ui/assets/monaco-vim.umd-DNcbBSgx.js → gitlatex-2.1.0/gitlatex/ui/assets/monaco-vim.umd-DBJdb7VY.js +2 -2
  21. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/index.html +2 -2
  22. {gitlatex-2.0.0 → gitlatex-2.1.0/gitlatex.egg-info}/PKG-INFO +6 -2
  23. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/SOURCES.txt +12 -5
  24. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/scm_file_list.json +13 -0
  25. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/scm_version.json +2 -2
  26. gitlatex-2.1.0/tests/test_scm.py +266 -0
  27. gitlatex-2.1.0/tests/test_sync.py +235 -0
  28. gitlatex-2.0.0/gitlatex/state.py +0 -27
  29. gitlatex-2.0.0/gitlatex/ui/assets/index-DzQbJ7aZ.css +0 -1
  30. gitlatex-2.0.0/gitlatex/ui/assets/index-Y8smC0mJ.js +0 -1938
  31. {gitlatex-2.0.0 → gitlatex-2.1.0}/.github/workflows/publish.yml +0 -0
  32. {gitlatex-2.0.0 → gitlatex-2.1.0}/.gitignore +0 -0
  33. {gitlatex-2.0.0 → gitlatex-2.1.0}/MANIFEST.in +0 -0
  34. {gitlatex-2.0.0 → gitlatex-2.1.0}/docs/vim-mode.md +0 -0
  35. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/__init__.py +0 -0
  36. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/__main__.py +0 -0
  37. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/app.py +0 -0
  38. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/engine/manifest.json +0 -0
  39. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/app.js +0 -0
  40. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/bib-commands.json +0 -0
  41. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/base.css +0 -0
  42. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/console.css +0 -0
  43. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/docs.css +0 -0
  44. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/editor.css +0 -0
  45. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/home.css +0 -0
  46. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/layout.css +0 -0
  47. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/loading.css +0 -0
  48. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/outline.css +0 -0
  49. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/problems.css +0 -0
  50. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/review.css +0 -0
  51. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/settings.css +0 -0
  52. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/sidebar.css +0 -0
  53. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/support.css +0 -0
  54. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/toolbar.css +0 -0
  55. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/updates.css +0 -0
  56. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/css/versions.css +0 -0
  57. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/favicon.svg +0 -0
  58. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/index.html +0 -0
  59. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/build/compile.js +0 -0
  60. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/build/problems.js +0 -0
  61. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/core/api.js +0 -0
  62. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/core/filetypes.js +0 -0
  63. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/core/router.js +0 -0
  64. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/core/state.js +0 -0
  65. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/core/storage.js +0 -0
  66. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/completions.js +0 -0
  67. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/envcolors.js +0 -0
  68. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/filetree.js +0 -0
  69. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/languages.js +0 -0
  70. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/mainfile.js +0 -0
  71. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/monaco.js +0 -0
  72. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/outline.js +0 -0
  73. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/projectindex.js +0 -0
  74. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/review.js +0 -0
  75. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/session.js +0 -0
  76. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/snippets.js +0 -0
  77. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/spell.js +0 -0
  78. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/synctex.js +0 -0
  79. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/vim.js +0 -0
  80. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/editor/vimtex.js +0 -0
  81. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/git/actions.js +0 -0
  82. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/git/diffview.js +0 -0
  83. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/git/menu.js +0 -0
  84. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/git/versions.js +0 -0
  85. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/home/repolist.js +0 -0
  86. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/consolepane.js +0 -0
  87. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/layout.js +0 -0
  88. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/loading.js +0 -0
  89. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/modals.js +0 -0
  90. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/pdfviewer.js +0 -0
  91. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/settings.js +0 -0
  92. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/theme.js +0 -0
  93. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/js/ui/viewer.js +0 -0
  94. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/public/latex-commands.json +0 -0
  95. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/engine.py +0 -0
  96. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/files.py +0 -0
  97. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/pages.py +0 -0
  98. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/projectindex.py +0 -0
  99. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/review.py +0 -0
  100. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/snippets.py +0 -0
  101. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/spell.py +0 -0
  102. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/synctex.py +0 -0
  103. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/routes/system.py +0 -0
  104. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/server.py +0 -0
  105. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/__init__.py +0 -0
  106. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/git_backend.py +0 -0
  107. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/gitrepo.py +0 -0
  108. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/latex.py +0 -0
  109. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/paths.py +0 -0
  110. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/projectindex.py +0 -0
  111. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/snippets.py +0 -0
  112. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/spell.py +0 -0
  113. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/synctex.py +0 -0
  114. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/synctex_reader.py +0 -0
  115. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/services/updates.py +0 -0
  116. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  117. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  118. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  119. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  120. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  121. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  122. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  123. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  124. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  125. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  126. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  127. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  128. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  129. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  130. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  131. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  132. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  133. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  134. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  135. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  136. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  137. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  138. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  139. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  140. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  141. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  142. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  143. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  144. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  145. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  146. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  147. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  148. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  149. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  150. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  151. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  152. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  153. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  154. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  155. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  156. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  157. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  158. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  159. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  160. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  161. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  162. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  163. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  164. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  165. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  166. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  167. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  168. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  169. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  170. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  171. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  172. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  173. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  174. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  175. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/codicon-DF1abBS2.ttf +0 -0
  176. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/editor.worker-Yd1NR0QT.js +0 -0
  177. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/material-symbols-outlined-DzyP15l8.woff2 +0 -0
  178. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/pdf.worker.min-BmVo14Nb.mjs +0 -0
  179. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/assets/tex.worker-5oo4mY5v.js +0 -0
  180. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex/ui/favicon.svg +0 -0
  181. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/dependency_links.txt +0 -0
  182. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/entry_points.txt +0 -0
  183. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/requires.txt +0 -0
  184. {gitlatex-2.0.0 → gitlatex-2.1.0}/gitlatex.egg-info/top_level.txt +0 -0
  185. {gitlatex-2.0.0 → gitlatex-2.1.0}/pyproject.toml +0 -0
  186. {gitlatex-2.0.0 → gitlatex-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlatex
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A simple UI for LaTeX projects with Git support. Clone repos, edit, compile, and push.
5
5
  License-Expression: ISC
6
6
  Project-URL: Homepage, https://github.com/abdussamiakanda/gitlatex
@@ -261,7 +261,11 @@ An Overleaf-style review panel for leaving comments on your text.
261
261
  - **Shared through Git.** Each thread is a small JSON file in
262
262
  `.gitlatex/comments/` inside the project, so comments are pushed and pulled
263
263
  with your files. Each thread has its own file, so two people starting threads
264
- at the same time won't get merge conflicts.
264
+ at the same time won't get merge conflicts, and replies made on both sides of
265
+ a pull are combined automatically (the app registers a git merge driver for
266
+ these files, so this also works for `git pull` in a terminal).
267
+ See [docs/sync-and-conflicts.md](docs/sync-and-conflicts.md) for how push,
268
+ pull and conflicts are handled.
265
269
  - Comments **stay attached to their text** as you edit, find it again after a
266
270
  pull, and follow a file when you rename it. Deleting a file removes its
267
271
  comments.
@@ -233,7 +233,11 @@ An Overleaf-style review panel for leaving comments on your text.
233
233
  - **Shared through Git.** Each thread is a small JSON file in
234
234
  `.gitlatex/comments/` inside the project, so comments are pushed and pulled
235
235
  with your files. Each thread has its own file, so two people starting threads
236
- at the same time won't get merge conflicts.
236
+ at the same time won't get merge conflicts, and replies made on both sides of
237
+ a pull are combined automatically (the app registers a git merge driver for
238
+ these files, so this also works for `git pull` in a terminal).
239
+ See [docs/sync-and-conflicts.md](docs/sync-and-conflicts.md) for how push,
240
+ pull and conflicts are handled.
237
241
  - Comments **stay attached to their text** as you edit, find it again after a
238
242
  pull, and follow a file when you rename it. Deleting a file removes its
239
243
  comments.
@@ -0,0 +1,206 @@
1
+ # Syncing with coauthors: push, pull and conflicts
2
+
3
+ gitlatex projects are shared through Git, so two people working at once
4
+ eventually change the same thing. This page covers what the app does
5
+ automatically when that happens, what it reports when it can't, and how to fix
6
+ the rest by hand.
7
+
8
+ - **Push** commits everything and uploads it. If a coauthor pushed first, the
9
+ app pulls their commits in, puts yours on top, and pushes again.
10
+ - **Pull** downloads the remote's commits and rebases your unpushed commits on
11
+ top of them. Uncommitted edits are set aside and put back afterwards.
12
+ - **Review comments** replied to on both sides are combined, never conflicted.
13
+ - **A real conflict** (both of you changed the same lines of a `.tex` file) is
14
+ never left half-done: the app undoes the pull, puts the project back as it
15
+ was, and lists the files involved.
16
+
17
+ The code lives in `gitlatex/routes/git.py` (push and pull),
18
+ `gitlatex/services/comments.py` and `gitlatex/services/comments_merge.py`
19
+ (review comment threads). Tests are in `tests/test_sync.py`.
20
+
21
+ ## Two people, one remote
22
+
23
+ Alice commits but doesn't push. Meanwhile Bob pushes:
24
+
25
+ ```
26
+ remote: X ── B
27
+ Alice: X ── A (committed, not pushed)
28
+ ```
29
+
30
+ Alice's push can't simply go through: it would throw away Bob's commit, so Git
31
+ rejects it (`[rejected] (fetch first)`). Nothing is lost; Alice has to take
32
+ Bob's commit first.
33
+
34
+ ### What Push does
35
+
36
+ 1. Stages and commits everything (`git add -A`, then commit with your message).
37
+ 2. Pushes. If the remote rejects it because it has newer commits:
38
+ 3. Pulls with rebase (see below), so the history becomes `X ── B ── A`.
39
+ 4. Pushes again.
40
+
41
+ When the two of you changed different parts of the project, one click does
42
+ all of it. The toast says *"Your coauthors' newer changes were pulled in
43
+ first"*, and the editor reloads the files that changed.
44
+
45
+ If step 3 hits a real conflict, the push stops with **409** and a message like:
46
+
47
+ > Could not push: someone else pushed changes to the same lines first. Changed
48
+ > on both sides: main.tex. Your commit is saved here but not pushed yet.
49
+
50
+ Your commit stays in your local history, and the project is exactly as it was
51
+ before you pressed Push.
52
+
53
+ If someone pushes again between steps 3 and 4, the push stops with *"the
54
+ remote changed again while pushing. Try again."*
55
+
56
+ ### What Pull does
57
+
58
+ Pull runs `git pull --rebase --autostash`:
59
+
60
+ - **`--rebase`**: your unpushed commits go on top of the remote's, with no
61
+ "Merge branch …" commit every time two people work at once.
62
+ - **`--autostash`**: uncommitted edits are set aside before the pull and put
63
+ back afterwards.
64
+
65
+ A pull is always either finished or fully undone, never left half-merged:
66
+
67
+ | Situation | Result |
68
+ | --- | --- |
69
+ | Different lines changed | Pulled. Your commits sit on top of the remote's. |
70
+ | Same lines changed in **commits** | Rebase aborted. 409 listing the files. |
71
+ | Same lines changed by **uncommitted** edits | Pull undone and your edits restored. 409 listing the files. |
72
+ | Only build output conflicts (`main.pdf`, `main.synctex.gz`, `.aux`, …) | Your copy is kept and the pull finishes. The next compile rewrites it anyway. |
73
+ | Review comment threads changed on both sides | Merged automatically (see below). |
74
+
75
+ ## Review comments
76
+
77
+ Each review thread is one JSON file in `.gitlatex/comments/<id>.json`, and a
78
+ reply appends to that file's `messages` list. So if Alice and Bob both reply to
79
+ the same thread before syncing, both change the end of the same list, and a
80
+ plain Git merge always conflicts there.
81
+
82
+ ### The merge driver
83
+
84
+ `gitlatex/services/comments_merge.py` is a
85
+ [Git merge driver](https://git-scm.com/docs/gitattributes#_defining_a_custom_merge_driver):
86
+ Git runs it instead of its usual line-by-line merge whenever both sides changed
87
+ the same thread file. It merges the fields like this:
88
+
89
+ | Field | Rule |
90
+ | --- | --- |
91
+ | `messages` | Union of both sides by `id`, sorted by `date`; the opening message stays first. A message in the common ancestor that one side no longer has was deleted there, so it stays deleted. |
92
+ | `resolved`, `resolvedBy`, `resolvedAt` | Treated as one change. The side that changed it wins; if both did, the later `resolvedAt` wins. |
93
+ | `range`, `quote`, `file` | The side that changed it wins, otherwise ours. The editor finds the text again from `quote` after a pull anyway. |
94
+ | Anything that isn't valid JSON | The driver exits with 1 and Git falls back to a normal conflict. |
95
+
96
+ ### How it is registered
97
+
98
+ `comments.install_merge_driver()` runs whenever a project is opened, selected
99
+ or cloned, and before every pull. It writes two things, both inside the
100
+ project's own `.git` folder, so nothing new shows up in the project's files or
101
+ commits:
102
+
103
+ ```ini
104
+ # .git/config
105
+ [merge "gitlatex-comments"]
106
+ name = gitlatex review comment threads
107
+ driver = '/path/to/python' '/path/to/gitlatex/services/comments_merge.py' %O %A %B
108
+ ```
109
+
110
+ ```
111
+ # .git/info/attributes
112
+ .gitlatex/comments/*.json merge=gitlatex-comments
113
+ ```
114
+
115
+ A merge driver is a command on one machine, so it can't be shared through the
116
+ repository: each copy of the project sets up its own. Once registered, it also
117
+ works for `git pull` and `git merge` in a terminal. If the app is moved or
118
+ reinstalled, the next time a project is opened the command is updated.
119
+
120
+ ### Threads with conflict markers
121
+
122
+ A thread file can still end up with `<<<<<<<` markers, for example from a merge
123
+ made on a machine without the app. Such a file isn't valid JSON. It used to be
124
+ skipped without a word, so the thread just disappeared from the panel. Now the
125
+ panel shows a yellow card in its place, *"This thread has a merge conflict"*,
126
+ with the path of the file. Renaming the commented file leaves the conflicted
127
+ thread file untouched.
128
+
129
+ ## Fixing a conflict by hand
130
+
131
+ The app has no screen for resolving conflicts in `.tex` files yet. When it
132
+ reports one, open a terminal in the project folder:
133
+
134
+ ```sh
135
+ git pull --rebase # stops at the first conflict
136
+ git status # lists the files marked "both modified"
137
+ ```
138
+
139
+ In each file, choose what to keep between the markers, then delete the marker
140
+ lines:
141
+
142
+ ```latex
143
+ <<<<<<< HEAD
144
+ Bob's version of the paragraph.
145
+ =======
146
+ Alice's version of the paragraph.
147
+ >>>>>>> Alice's commit
148
+ ```
149
+
150
+ During a rebase, the `HEAD` side is the remote (Bob), and the other side is
151
+ your commit being replayed. Then:
152
+
153
+ ```sh
154
+ git add main.tex
155
+ git rebase --continue # repeat for each stopped commit
156
+ git push
157
+ ```
158
+
159
+ `git rebase --abort` goes back to where you started at any point.
160
+
161
+ **A conflicted comment thread** (only possible without the merge driver):
162
+ merge the two `messages` lists, keeping every message from both sides in date
163
+ order. Check the commas between entries and remove the marker lines. Take
164
+ `range` and `quote` from either side.
165
+
166
+ ```json
167
+ "messages": [
168
+ { "id": "a1…", "author": "Alice", "date": "2026-09-29T10:00:00", "text": "Is this right?" },
169
+ { "id": "b2…", "author": "Bob", "date": "2026-09-29T11:00:00", "text": "Yes" },
170
+ { "id": "c3…", "author": "Alice", "date": "2026-09-29T11:05:00", "text": "Also check eq. 3" }
171
+ ]
172
+ ```
173
+
174
+ ## What changed
175
+
176
+ Before these changes:
177
+
178
+ - **Push reported success when the push was rejected.** GitPython reports a
179
+ rejected push in its result instead of raising, and the result wasn't
180
+ checked. The commit never reached the remote, and nobody was told.
181
+ - **Pull failed when both sides had new commits**, with *"Need to specify how
182
+ to reconcile divergent branches"* (Git 2.27 and later, when `pull.rebase`
183
+ isn't set).
184
+ - **Uncommitted edits on the same lines** as incoming changes left conflict
185
+ markers inside the `.tex` file, while the pull reported success.
186
+ - **Replies on both sides of a comment thread** always conflicted, and the
187
+ thread then disappeared from the review panel.
188
+
189
+ | File | Change |
190
+ | --- | --- |
191
+ | `gitlatex/routes/git.py` | `_push()` detects rejected pushes. `_pull_rebase()` pulls with rebase, keeps local build output, and undoes anything that conflicts. Push retries after pulling. Conflicts return 409 with a `conflicts` list. |
192
+ | `gitlatex/services/comments_merge.py` | New: the merge driver for comment threads. |
193
+ | `gitlatex/services/comments.py` | `install_merge_driver()`. Conflicted thread files are listed as placeholders instead of being skipped. |
194
+ | `gitlatex/routes/repos.py`, `gitlatex/routes/workspace.py` | Register the merge driver when a project is opened, selected or cloned. |
195
+ | `web/src/storage/server.ts`, `web/src/state/actions.ts` | Push reports `pulled`. The editor reloads and says so. |
196
+ | `web/src/editor/review.ts`, `web/src/editor/review.css` | The merge-conflict card in the review panel. |
197
+ | `tests/test_sync.py` | New: unit tests for the merge driver, and two-clone tests of push and pull through the app. |
198
+
199
+ Run the tests with:
200
+
201
+ ```sh
202
+ python3 -m unittest discover tests
203
+ ```
204
+
205
+ The classic editor at `/classic` shows the new push and pull messages, but its
206
+ review panel doesn't have the merge-conflict card.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.0'
22
- __version_tuple__ = version_tuple = (2, 0, 0)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gf67d4da27'
24
+ __commit_id__ = commit_id = 'g71d35ed41'
@@ -1,7 +1,11 @@
1
1
  """Small helpers and constants shared by every route module."""
2
2
 
3
+ import functools
4
+
3
5
  from flask import request
4
6
 
7
+ from gitlatex import state
8
+
5
9
  MIME_TYPES = {
6
10
  ".pdf": "application/pdf",
7
11
  ".png": "image/png",
@@ -29,6 +33,15 @@ ASSET_EXTENSIONS = frozenset({".css", ".js", ".mjs", ".svg", ".ico", ".png", ".j
29
33
  ".woff2", ".ttf", ".wasm"})
30
34
 
31
35
 
36
+ def with_project_lock(view):
37
+ """Run a route while holding state.project_lock (see gitlatex/state.py)."""
38
+ @functools.wraps(view)
39
+ def wrapper(*args, **kwargs):
40
+ with state.project_lock:
41
+ return view(*args, **kwargs)
42
+ return wrapper
43
+
44
+
32
45
  def _json():
33
46
  """Request JSON body; default to empty dict."""
34
47
  return request.get_json(force=True, silent=True) or {}
@@ -14,6 +14,7 @@ blueprint below - nothing else in the app needs to know about it.
14
14
  snippets the user's own editor snippets
15
15
  review comment threads on lines of project files
16
16
  git push, pull, status, history and diffs
17
+ scm the Source control panel: staging, conflicts, branches, publish
17
18
  workspace the file API behind the editor UI in web/
18
19
  engine the in-browser TeX engine and its package shelf
19
20
 
@@ -30,6 +31,7 @@ from gitlatex.routes import (
30
31
  projectindex,
31
32
  repos,
32
33
  review,
34
+ scm,
33
35
  snippets,
34
36
  spell,
35
37
  synctex,
@@ -48,6 +50,7 @@ BLUEPRINTS = (
48
50
  snippets.bp,
49
51
  review.bp,
50
52
  git.bp,
53
+ scm.bp,
51
54
  workspace.bp,
52
55
  engine.bp,
53
56
  pages.bp,
@@ -7,7 +7,7 @@ import subprocess
7
7
  from flask import Blueprint, jsonify, request, send_file
8
8
 
9
9
  from gitlatex import state
10
- from gitlatex.http import _json
10
+ from gitlatex.http import _json, with_project_lock
11
11
  from gitlatex.services.latex import LATEX_ENGINES, run_latex_build
12
12
  from gitlatex.services.paths import resolve_repo_path
13
13
  from gitlatex.services.synctex import save_remote_synctex
@@ -16,6 +16,7 @@ bp = Blueprint("compile", __name__)
16
16
 
17
17
 
18
18
  @bp.route("/compile", methods=["GET", "POST"])
19
+ @with_project_lock
19
20
  def compile_latex():
20
21
  if request.method == "GET":
21
22
  return jsonify(
@@ -111,11 +112,13 @@ def handle_save_pdf():
111
112
 
112
113
 
113
114
  @bp.route("/save-pdf", methods=["POST"])
115
+ @with_project_lock
114
116
  def save_pdf():
115
117
  return handle_save_pdf()
116
118
 
117
119
 
118
120
  @bp.route("/api/save-pdf", methods=["POST"])
121
+ @with_project_lock
119
122
  def api_save_pdf():
120
123
  return handle_save_pdf()
121
124
 
@@ -6,14 +6,17 @@ import os
6
6
  from flask import Blueprint, jsonify, request
7
7
 
8
8
  from gitlatex import state
9
+ from gitlatex.http import with_project_lock
9
10
  from gitlatex.services.git_backend import NULL_TREE, GitCommandError, Repo
10
11
  from gitlatex.services.gitrepo import _blob_text, _close_repo, _numstat, _open_repo
11
12
  from gitlatex.services.paths import resolve_repo_path
13
+ from gitlatex.services.sync import Conflict, clear_stale_build_outputs, pull_rebase, push_current
12
14
 
13
15
  bp = Blueprint("git", __name__)
14
16
 
15
17
 
16
18
  @bp.route("/push", methods=["POST"])
19
+ @with_project_lock
17
20
  def push():
18
21
  if not state.current_repo_path:
19
22
  return jsonify(error="No repository selected"), 400
@@ -38,9 +41,24 @@ def push():
38
41
  repo.index.commit(message)
39
42
  committed = True
40
43
  print("Commit:", message)
41
- repo.remotes.origin.push()
44
+ pulled = False
45
+ if not push_current(repo):
46
+ # Someone pushed first. Put our commits on top of theirs and
47
+ # try once more, so the common case needs no second click.
48
+ print("Push rejected; pulling first")
49
+ try:
50
+ pull_rebase(repo)
51
+ except Conflict as c:
52
+ return _conflict_response(c, committed,
53
+ "Could not push: someone else pushed changes to the same lines first.")
54
+ pulled = True
55
+ if not push_current(repo):
56
+ return jsonify(
57
+ error="Could not push: the remote changed again while pushing. Try again.",
58
+ committed=committed, pulled=True,
59
+ ), 409
42
60
  print("Pushed to origin")
43
- return jsonify(success=True, committed=committed)
61
+ return jsonify(success=True, committed=committed, pulled=pulled)
44
62
  finally:
45
63
  if getattr(repo, "close", None):
46
64
  try:
@@ -52,6 +70,7 @@ def push():
52
70
 
53
71
 
54
72
  @bp.route("/api/git/commit", methods=["POST"])
73
+ @with_project_lock
55
74
  def commit_all():
56
75
  """Stage everything and commit, without pushing (for projects with no remote)."""
57
76
  if not state.current_repo_path:
@@ -79,6 +98,7 @@ def commit_all():
79
98
 
80
99
 
81
100
  @bp.route("/api/git/init", methods=["POST"])
101
+ @with_project_lock
82
102
  def init_repo():
83
103
  if not state.current_repo_path:
84
104
  return jsonify(error="No repository selected"), 400
@@ -97,7 +117,20 @@ def _json_message():
97
117
  return message.strip() if isinstance(message, str) else ""
98
118
 
99
119
 
120
+ def _conflict_response(conflict, committed, headline):
121
+ """409 for a pull that was undone because of conflicting edits."""
122
+ files = conflict.files
123
+ listed = ", ".join(files) if files else "some files"
124
+ kept = "Your commit is saved here but not pushed yet." if committed else "Nothing was changed on your side."
125
+ return jsonify(
126
+ error="%s Changed on both sides: %s. %s Resolve it in a terminal with `git pull --rebase`, "
127
+ "or agree with your coauthor which version to keep." % (headline, listed, kept),
128
+ conflicts=files, committed=committed,
129
+ ), 409
130
+
131
+
100
132
  @bp.route("/pull", methods=["POST"])
133
+ @with_project_lock
101
134
  def pull():
102
135
  if not state.current_repo_path:
103
136
  return jsonify(error="No repository selected"), 400
@@ -107,15 +140,14 @@ def pull():
107
140
  repo = Repo(state.current_repo_path)
108
141
  try:
109
142
  before = repo.head.commit.hexsha if repo.head.is_valid() else None
110
- # --autostash keeps working-tree noise (compile artifacts like main.pdf)
111
- # from blocking the pull; older gits don't support it for merges.
143
+ cleared = clear_stale_build_outputs(repo)
112
144
  try:
113
- output = repo.git.pull("--autostash")
114
- except GitCommandError:
115
- output = repo.git.pull()
145
+ output = pull_rebase(repo)
146
+ except Conflict as c:
147
+ return _conflict_response(c, False, "Could not pull: you and the remote changed the same lines.")
116
148
  after = repo.head.commit.hexsha if repo.head.is_valid() else None
117
- print("Pulled from origin")
118
- return jsonify(success=True, output=output, changed=before != after)
149
+ print("Pulled from origin" + (" (replaced local build output: %s)" % ", ".join(cleared) if cleared else ""))
150
+ return jsonify(success=True, output=output, changed=before != after, cleared=cleared)
119
151
  finally:
120
152
  if getattr(repo, "close", None):
121
153
  try:
@@ -10,7 +10,7 @@ import sys
10
10
  from flask import Blueprint, jsonify
11
11
 
12
12
  from gitlatex import state
13
- from gitlatex.services import paths
13
+ from gitlatex.services import comments, paths
14
14
  from gitlatex.services.git_backend import Repo, git_rmtree
15
15
  from gitlatex.http import _json
16
16
  from gitlatex.services.paths import count_files_in_dir, parse_owner_from_remote_url
@@ -194,6 +194,8 @@ def select_repo():
194
194
  state.current_repo_path = repo_path
195
195
  git_dir = os.path.join(repo_path, ".git")
196
196
  has_git = os.path.isdir(git_dir)
197
+ if has_git:
198
+ comments.install_merge_driver(repo_path)
197
199
  print("Selected repo:", name)
198
200
  return jsonify(success=True, hasGit=has_git)
199
201
 
@@ -212,6 +214,7 @@ def clone_repo():
212
214
  try:
213
215
  Repo.clone_from(repo_url, repo_path)
214
216
  print("Cloned", repo_name)
217
+ comments.install_merge_driver(repo_path)
215
218
  state.current_repo_path = repo_path
216
219
  return jsonify(success=True)
217
220
  except Exception as e:
@@ -0,0 +1,221 @@
1
+ """Source control API for the editor's Source control panel (services/scm.py).
2
+
3
+ GET /api/scm/status branch, upstream, ahead/behind, staged/changes/conflicts
4
+ POST /api/scm/stage {paths?} stage files (all when omitted)
5
+ POST /api/scm/unstage {paths?}
6
+ POST /api/scm/discard {paths} drop working-tree changes
7
+ POST /api/scm/resolve {path, side: current|incoming}
8
+ POST /api/scm/mark-resolved {path}
9
+ POST /api/scm/abort abort the merge / rebase in progress
10
+ POST /api/scm/commit {message}
11
+ POST /api/scm/pull | push | sync | fetch
12
+ GET /api/scm/branches POST /api/scm/checkout {name, create?}
13
+ GET /api/scm/publish-options POST /api/scm/publish {url? | name?, private?}
14
+ GET /api/scm/diff?path=&staged=1
15
+
16
+ Pull and push rebase like /pull and /push (services/sync.py), but a real
17
+ conflict is left in the files for the editor to resolve. Everything that
18
+ changes files holds state.project_lock, like the editor's writes.
19
+ """
20
+
21
+ import functools
22
+
23
+ from flask import Blueprint, jsonify, request
24
+
25
+ from gitlatex import state
26
+ from gitlatex.http import _json, with_project_lock
27
+ from gitlatex.services import scm
28
+ from gitlatex.services.git_backend import GitCommandError, Repo
29
+ from gitlatex.services.gitrepo import _close_repo
30
+
31
+ bp = Blueprint("scm", __name__)
32
+
33
+
34
+ def _git_error(e):
35
+ """The useful part of a failed git command: its stderr, without GitPython's framing."""
36
+ text = (getattr(e, "stderr", "") or str(e)).strip()
37
+ if text.startswith("stderr:"):
38
+ text = text[len("stderr:"):]
39
+ return text.strip().strip("'").strip() or str(e)
40
+
41
+
42
+ def with_repo(view):
43
+ """Open the current project's repository, pass it in, and turn failures into JSON errors."""
44
+
45
+ @functools.wraps(view)
46
+ def wrapper(*args, **kwargs):
47
+ if not state.current_repo_path:
48
+ return jsonify(error="No project open"), 400
49
+ if Repo is None:
50
+ return jsonify(error="GitPython not installed"), 500
51
+ try:
52
+ repo = Repo(state.current_repo_path)
53
+ except Exception:
54
+ return jsonify(error="This project is not a Git repository."), 400
55
+ try:
56
+ return view(repo, *args, **kwargs)
57
+ except ValueError as e:
58
+ return jsonify(error=str(e)), 400
59
+ except GitCommandError as e:
60
+ return jsonify(error=_git_error(e)), 500
61
+ finally:
62
+ _close_repo(repo)
63
+
64
+ return wrapper
65
+
66
+
67
+ def _paths():
68
+ paths = _json().get("paths")
69
+ return [p for p in paths if isinstance(p, str) and p] if isinstance(paths, list) else None
70
+
71
+
72
+ def _ok(repo, **extra):
73
+ """Every change returns the fresh status, so the panel redraws from one response."""
74
+ return jsonify(success=True, status=scm.status(repo), **extra)
75
+
76
+
77
+ @bp.route("/api/scm/status")
78
+ @with_repo
79
+ def get_status(repo):
80
+ return jsonify(scm.status(repo))
81
+
82
+
83
+ @bp.route("/api/scm/stage", methods=["POST"])
84
+ @with_project_lock
85
+ @with_repo
86
+ def stage(repo):
87
+ scm.stage(repo, _paths())
88
+ return _ok(repo)
89
+
90
+
91
+ @bp.route("/api/scm/unstage", methods=["POST"])
92
+ @with_project_lock
93
+ @with_repo
94
+ def unstage(repo):
95
+ scm.unstage(repo, _paths(), has_commits=scm.status(repo)["hasCommits"])
96
+ return _ok(repo)
97
+
98
+
99
+ @bp.route("/api/scm/discard", methods=["POST"])
100
+ @with_project_lock
101
+ @with_repo
102
+ def discard(repo):
103
+ paths = _paths()
104
+ if not paths:
105
+ raise ValueError("No files given")
106
+ scm.discard(repo, paths)
107
+ return _ok(repo)
108
+
109
+
110
+ @bp.route("/api/scm/resolve", methods=["POST"])
111
+ @with_project_lock
112
+ @with_repo
113
+ def resolve(repo):
114
+ data = _json()
115
+ side = data.get("side")
116
+ if side not in ("current", "incoming") or not data.get("path"):
117
+ raise ValueError("Give a path and side: current or incoming")
118
+ scm.take_side(repo, data["path"], side)
119
+ return _ok(repo)
120
+
121
+
122
+ @bp.route("/api/scm/mark-resolved", methods=["POST"])
123
+ @with_project_lock
124
+ @with_repo
125
+ def mark_resolved(repo):
126
+ path = _json().get("path")
127
+ if not path:
128
+ raise ValueError("No path given")
129
+ scm.mark_resolved(repo, path)
130
+ return _ok(repo)
131
+
132
+
133
+ @bp.route("/api/scm/abort", methods=["POST"])
134
+ @with_project_lock
135
+ @with_repo
136
+ def abort(repo):
137
+ scm.abort(repo)
138
+ return _ok(repo)
139
+
140
+
141
+ @bp.route("/api/scm/commit", methods=["POST"])
142
+ @with_project_lock
143
+ @with_repo
144
+ def commit(repo):
145
+ return _ok(repo, **scm.commit(repo, (_json().get("message") or "").strip()))
146
+
147
+
148
+ @bp.route("/api/scm/pull", methods=["POST"])
149
+ @with_project_lock
150
+ @with_repo
151
+ def pull(repo):
152
+ return _ok(repo, **scm.pull(repo))
153
+
154
+
155
+ @bp.route("/api/scm/push", methods=["POST"])
156
+ @with_project_lock
157
+ @with_repo
158
+ def push(repo):
159
+ return _ok(repo, **scm.push(repo))
160
+
161
+
162
+ @bp.route("/api/scm/sync", methods=["POST"])
163
+ @with_project_lock
164
+ @with_repo
165
+ def sync(repo):
166
+ """Pull, then push if the pull left nothing to resolve (VS Code's Sync Changes)."""
167
+ result = scm.pull(repo)
168
+ if not result["conflicts"] and scm.status(repo)["ahead"]:
169
+ pushed = scm.push(repo)
170
+ result = {**pushed, "cleared": result["cleared"] + pushed["cleared"]}
171
+ return _ok(repo, **result)
172
+
173
+
174
+ @bp.route("/api/scm/fetch", methods=["POST"])
175
+ @with_repo
176
+ def fetch(repo):
177
+ if repo.remotes:
178
+ repo.git.fetch("--all", "--prune")
179
+ return _ok(repo)
180
+
181
+
182
+ @bp.route("/api/scm/branches")
183
+ @with_repo
184
+ def branches(repo):
185
+ return jsonify(scm.branches(repo))
186
+
187
+
188
+ @bp.route("/api/scm/checkout", methods=["POST"])
189
+ @with_project_lock
190
+ @with_repo
191
+ def checkout(repo):
192
+ data = _json()
193
+ name = (data.get("name") or "").strip()
194
+ if not name:
195
+ raise ValueError("No branch name given")
196
+ scm.checkout(repo, name, create=bool(data.get("create")))
197
+ return _ok(repo)
198
+
199
+
200
+ @bp.route("/api/scm/publish-options")
201
+ def publish_options():
202
+ return jsonify(gh=scm.gh_ready())
203
+
204
+
205
+ @bp.route("/api/scm/publish", methods=["POST"])
206
+ @with_project_lock
207
+ @with_repo
208
+ def publish(repo):
209
+ data = _json()
210
+ url = (data.get("url") or "").strip() or None
211
+ output = scm.publish(repo, url=url, name=data.get("name"), private=data.get("private", True) is not False)
212
+ return _ok(repo, output=output)
213
+
214
+
215
+ @bp.route("/api/scm/diff")
216
+ @with_repo
217
+ def diff(repo):
218
+ path = (request.args.get("path") or "").replace("\\", "/")
219
+ if not path:
220
+ raise ValueError("No path given")
221
+ return jsonify(scm.diff(repo, path, staged=request.args.get("staged") in ("1", "true")))