gitlatex 2.0.1__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.
- {gitlatex-2.0.1/gitlatex.egg-info → gitlatex-2.1.0}/PKG-INFO +6 -2
- {gitlatex-2.0.1 → gitlatex-2.1.0}/README.md +5 -1
- gitlatex-2.1.0/docs/sync-and-conflicts.md +206 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/_version.py +3 -3
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/__init__.py +3 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/git.py +32 -63
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/repos.py +4 -1
- gitlatex-2.1.0/gitlatex/routes/scm.py +221 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/workspace.py +2 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/comments.py +83 -1
- gitlatex-2.1.0/gitlatex/services/comments_merge.py +106 -0
- gitlatex-2.1.0/gitlatex/services/scm.py +406 -0
- gitlatex-2.1.0/gitlatex/services/sync.py +168 -0
- gitlatex-2.0.1/gitlatex/ui/assets/PdfViewer-CSeWHO6L.js → gitlatex-2.1.0/gitlatex/ui/assets/PdfViewer-CYPYydmG.js +2 -2
- gitlatex-2.1.0/gitlatex/ui/assets/index-Bd4rVZK8.css +1 -0
- gitlatex-2.1.0/gitlatex/ui/assets/index-DL-0rlAe.js +2179 -0
- gitlatex-2.0.1/gitlatex/ui/assets/monaco-vim.umd-TnvlvL5k.js → gitlatex-2.1.0/gitlatex/ui/assets/monaco-vim.umd-DBJdb7VY.js +2 -2
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/index.html +2 -2
- {gitlatex-2.0.1 → gitlatex-2.1.0/gitlatex.egg-info}/PKG-INFO +6 -2
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/SOURCES.txt +12 -5
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/scm_file_list.json +13 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/scm_version.json +2 -2
- gitlatex-2.1.0/tests/test_scm.py +266 -0
- gitlatex-2.1.0/tests/test_sync.py +235 -0
- gitlatex-2.0.1/gitlatex/ui/assets/index-CRd_ihCz.js +0 -1938
- gitlatex-2.0.1/gitlatex/ui/assets/index-DzQbJ7aZ.css +0 -1
- {gitlatex-2.0.1 → gitlatex-2.1.0}/.github/workflows/publish.yml +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/.gitignore +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/MANIFEST.in +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/docs/vim-mode.md +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/__init__.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/__main__.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/app.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/engine/manifest.json +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/http.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/app.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/bib-commands.json +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/base.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/console.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/docs.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/editor.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/home.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/layout.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/loading.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/outline.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/problems.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/review.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/settings.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/sidebar.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/support.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/toolbar.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/updates.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/css/versions.css +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/favicon.svg +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/index.html +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/build/compile.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/build/problems.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/core/api.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/core/filetypes.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/core/router.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/core/state.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/core/storage.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/completions.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/envcolors.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/filetree.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/languages.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/mainfile.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/monaco.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/outline.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/projectindex.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/review.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/session.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/snippets.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/spell.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/synctex.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/vim.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/editor/vimtex.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/git/actions.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/git/diffview.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/git/menu.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/git/versions.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/home/repolist.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/consolepane.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/layout.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/loading.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/modals.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/pdfviewer.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/settings.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/theme.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/js/ui/viewer.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/public/latex-commands.json +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/compile.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/engine.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/files.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/pages.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/projectindex.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/review.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/snippets.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/spell.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/synctex.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/routes/system.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/server.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/__init__.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/git_backend.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/gitrepo.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/latex.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/paths.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/projectindex.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/snippets.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/spell.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/synctex.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/synctex_reader.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/services/updates.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/state.py +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/codicon-DF1abBS2.ttf +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/editor.worker-Yd1NR0QT.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/material-symbols-outlined-DzyP15l8.woff2 +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/pdf.worker.min-BmVo14Nb.mjs +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/assets/tex.worker-5oo4mY5v.js +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex/ui/favicon.svg +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/dependency_links.txt +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/entry_points.txt +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/requires.txt +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/gitlatex.egg-info/top_level.txt +0 -0
- {gitlatex-2.0.1 → gitlatex-2.1.0}/pyproject.toml +0 -0
- {gitlatex-2.0.1 → 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
|
|
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
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g71d35ed41'
|
|
@@ -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,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Git operations: push, pull, status, history and diffs."""
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
-
import re
|
|
5
4
|
import os
|
|
6
5
|
|
|
7
6
|
from flask import Blueprint, jsonify, request
|
|
@@ -11,6 +10,7 @@ from gitlatex.http import with_project_lock
|
|
|
11
10
|
from gitlatex.services.git_backend import NULL_TREE, GitCommandError, Repo
|
|
12
11
|
from gitlatex.services.gitrepo import _blob_text, _close_repo, _numstat, _open_repo
|
|
13
12
|
from gitlatex.services.paths import resolve_repo_path
|
|
13
|
+
from gitlatex.services.sync import Conflict, clear_stale_build_outputs, pull_rebase, push_current
|
|
14
14
|
|
|
15
15
|
bp = Blueprint("git", __name__)
|
|
16
16
|
|
|
@@ -41,9 +41,24 @@ def push():
|
|
|
41
41
|
repo.index.commit(message)
|
|
42
42
|
committed = True
|
|
43
43
|
print("Commit:", message)
|
|
44
|
-
|
|
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
|
|
45
60
|
print("Pushed to origin")
|
|
46
|
-
return jsonify(success=True, committed=committed)
|
|
61
|
+
return jsonify(success=True, committed=committed, pulled=pulled)
|
|
47
62
|
finally:
|
|
48
63
|
if getattr(repo, "close", None):
|
|
49
64
|
try:
|
|
@@ -102,58 +117,16 @@ def _json_message():
|
|
|
102
117
|
return message.strip() if isinstance(message, str) else ""
|
|
103
118
|
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return True
|
|
116
|
-
# A PDF is build output only when it is a document's own PDF (main.pdf next
|
|
117
|
-
# to main.tex); figure PDFs are the user's files.
|
|
118
|
-
if path.lower().endswith(".pdf"):
|
|
119
|
-
return os.path.isfile(os.path.join(root, path[:-4] + ".tex"))
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _clear_stale_build_outputs(repo):
|
|
124
|
-
"""Drop local build outputs that the incoming commits replace.
|
|
125
|
-
|
|
126
|
-
Compiling rewrites main.pdf (PDFs embed a timestamp) and writes
|
|
127
|
-
main.synctex.gz, so a project that commits them always looks modified, and
|
|
128
|
-
`git pull` refuses to overwrite them. They are regenerated by the next
|
|
129
|
-
compile anyway, so the remote's copies win. Only files the remote actually
|
|
130
|
-
changes are touched; the user's own edits are left to --autostash.
|
|
131
|
-
Returns the paths cleared.
|
|
132
|
-
"""
|
|
133
|
-
try:
|
|
134
|
-
if not repo.remotes or repo.head.is_detached or repo.active_branch.tracking_branch() is None:
|
|
135
|
-
return []
|
|
136
|
-
repo.git.fetch("--quiet")
|
|
137
|
-
incoming = repo.git.diff("--name-only", "HEAD...@{u}").splitlines()
|
|
138
|
-
except (GitCommandError, TypeError, ValueError):
|
|
139
|
-
return []
|
|
140
|
-
root = repo.working_tree_dir
|
|
141
|
-
modified = set(repo.git.diff("--name-only").splitlines()) | set(repo.git.diff("--name-only", "--cached").splitlines())
|
|
142
|
-
untracked = set(repo.untracked_files)
|
|
143
|
-
cleared = []
|
|
144
|
-
for path in incoming:
|
|
145
|
-
if not _is_build_output(root, path):
|
|
146
|
-
continue
|
|
147
|
-
try:
|
|
148
|
-
if path in modified:
|
|
149
|
-
repo.git.checkout("HEAD", "--", path)
|
|
150
|
-
cleared.append(path)
|
|
151
|
-
elif path in untracked:
|
|
152
|
-
os.remove(os.path.join(root, path))
|
|
153
|
-
cleared.append(path)
|
|
154
|
-
except (GitCommandError, OSError) as e:
|
|
155
|
-
print("Could not clear", path, e)
|
|
156
|
-
return cleared
|
|
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
|
|
157
130
|
|
|
158
131
|
|
|
159
132
|
@bp.route("/pull", methods=["POST"])
|
|
@@ -167,15 +140,11 @@ def pull():
|
|
|
167
140
|
repo = Repo(state.current_repo_path)
|
|
168
141
|
try:
|
|
169
142
|
before = repo.head.commit.hexsha if repo.head.is_valid() else None
|
|
170
|
-
cleared =
|
|
171
|
-
# --autostash sets the user's own uncommitted edits aside and puts
|
|
172
|
-
# them back after the merge; older gits don't support it for merges.
|
|
143
|
+
cleared = clear_stale_build_outputs(repo)
|
|
173
144
|
try:
|
|
174
|
-
output = repo
|
|
175
|
-
except
|
|
176
|
-
|
|
177
|
-
raise
|
|
178
|
-
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.")
|
|
179
148
|
after = repo.head.commit.hexsha if repo.head.is_valid() else None
|
|
180
149
|
print("Pulled from origin" + (" (replaced local build output: %s)" % ", ".join(cleared) if cleared else ""))
|
|
181
150
|
return jsonify(success=True, output=output, changed=before != after, cleared=cleared)
|
|
@@ -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")))
|
|
@@ -115,6 +115,8 @@ def open_project():
|
|
|
115
115
|
state.current_repo_path = full
|
|
116
116
|
files, folders = _read_project(full)
|
|
117
117
|
has_git = os.path.isdir(os.path.join(full, ".git"))
|
|
118
|
+
if has_git:
|
|
119
|
+
comments.install_merge_driver(full)
|
|
118
120
|
print("Opened project:", os.path.basename(full))
|
|
119
121
|
return jsonify(name=os.path.basename(full), hasGit=has_git, files=files, folders=folders)
|
|
120
122
|
|