abstra 3.23.12__py3-none-any.whl → 3.24.1__py3-none-any.whl
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.
- abstra/cli.py +16 -5
- {abstra-3.23.12.dist-info → abstra-3.24.1.dist-info}/METADATA +1 -1
- {abstra-3.23.12.dist-info → abstra-3.24.1.dist-info}/RECORD +200 -190
- abstra_internals/cloud_api/__init__.py +17 -8
- abstra_internals/consts/filepaths.py +1 -1
- abstra_internals/contracts_generated.py +616 -596
- abstra_internals/controllers/ai.py +89 -1
- abstra_internals/controllers/git.py +307 -0
- abstra_internals/controllers/main.py +10 -6
- abstra_internals/environment.py +6 -0
- abstra_internals/interface/cli/deploy.py +1 -1
- abstra_internals/interface/cli/editor.py +2 -2
- abstra_internals/interface/sdk/ai.py +1 -0
- abstra_internals/repositories/git/__init__.py +25 -0
- abstra_internals/repositories/git/git_test.py +362 -0
- abstra_internals/repositories/git/native.py +578 -0
- abstra_internals/repositories/git/types.py +273 -0
- abstra_internals/repositories/linter/rules/env_in_bundle.py +5 -5
- abstra_internals/repositories/linter/rules/env_in_bundle_test.py +6 -6
- abstra_internals/repositories/linter/rules/venv_in_bundle.py +9 -16
- abstra_internals/server/blueprints/editor.py +4 -0
- abstra_internals/server/routes/git.py +190 -0
- abstra_internals/server/routes/workspace.py +1 -1
- abstra_internals/services/file_watcher.py +32 -13
- abstra_internals/services/fs.py +4 -4
- abstra_internals/services/fs_test.py +4 -6
- abstra_internals/templates/__init__.py +0 -11
- abstra_statics/dist/assets/{AbstraButton.vue_vue_type_script_setup_true_lang.441fcdfd.js → AbstraButton.vue_vue_type_script_setup_true_lang.aefce2d3.js} +2 -2
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.28bc9fc9.js +2 -0
- abstra_statics/dist/assets/ApiKeys.3c1e70dc.js +2 -0
- abstra_statics/dist/assets/App.9d8bd2aa.js +2 -0
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.a64c7cee.js +2 -0
- abstra_statics/dist/assets/{BaseLayout.c63dfc2d.js → BaseLayout.72a6c8f2.js} +2 -2
- abstra_statics/dist/assets/{Billing.461db3ef.js → Billing.6b07f282.js} +2 -2
- abstra_statics/dist/assets/{Breadcrumb.27110ec4.js → Breadcrumb.5c786c09.js} +2 -2
- abstra_statics/dist/assets/{Builds.4eecd717.js → Builds.d8c5c61b.js} +2 -2
- abstra_statics/dist/assets/{Card.1d1a9fb7.js → Card.fc77085c.js} +2 -2
- abstra_statics/dist/assets/{CircularLoading.fc66331b.js → CircularLoading.998b223a.js} +2 -2
- abstra_statics/dist/assets/{CloseCircleOutlined.0110bbe2.js → CloseCircleOutlined.9ff269cc.js} +2 -2
- abstra_statics/dist/assets/{ConnectorsView.82e74ae4.css → ConnectorsView.33c5380f.css} +1 -1
- abstra_statics/dist/assets/ConnectorsView.e670738b.js +2 -0
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.3ca66327.js +2 -0
- abstra_statics/dist/assets/ContentLayout.0db8d7fb.js +2 -0
- abstra_statics/dist/assets/{CrudView.c16e2f81.js → CrudView.cc6aeaf0.js} +2 -2
- abstra_statics/dist/assets/DocsButton.vue_vue_type_script_setup_true_lang.689d8ce7.js +2 -0
- abstra_statics/dist/assets/{EditorLogin.00a3b3d9.js → EditorLogin.691452b4.js} +2 -2
- abstra_statics/dist/assets/{EditorsView.f7b2843c.js → EditorsView.fcdf0186.js} +2 -2
- abstra_statics/dist/assets/EnvVars.5519b4ce.js +2 -0
- abstra_statics/dist/assets/Error.11fd1f21.js +2 -0
- abstra_statics/dist/assets/ExclamationCircleOutlined.d0aa47da.js +2 -0
- abstra_statics/dist/assets/Files.6ea18d46.js +2 -0
- abstra_statics/dist/assets/{Form.8077681f.js → Form.09de384c.js} +2 -2
- abstra_statics/dist/assets/{FormRunner.055e2c45.js → FormRunner.15be6fa0.js} +2 -2
- abstra_statics/dist/assets/{Home.0ef22910.js → Home.782c5702.js} +2 -2
- abstra_statics/dist/assets/Home.7c495aa9.js +2 -0
- abstra_statics/dist/assets/{Live.e1261458.js → Live.48c10605.js} +2 -2
- abstra_statics/dist/assets/LoadingContainer.82ac3d44.js +2 -0
- abstra_statics/dist/assets/LoadingOutlined.66639b08.js +2 -0
- abstra_statics/dist/assets/Login.4a05f5c5.js +2 -0
- abstra_statics/dist/assets/{Login.041361ea.js → Login.4e852955.js} +2 -2
- abstra_statics/dist/assets/{Login.vue_vue_type_script_setup_true_lang.c2eb444c.js → Login.vue_vue_type_script_setup_true_lang.5161f328.js} +2 -2
- abstra_statics/dist/assets/Logo.65e0b37c.js +2 -0
- abstra_statics/dist/assets/{Logs.f76cde12.js → Logs.7d1acf32.js} +2 -2
- abstra_statics/dist/assets/{LogsController.addd81bf.js → LogsController.0eb2eb30.js} +2 -2
- abstra_statics/dist/assets/Main.0313f4fc.js +2 -0
- abstra_statics/dist/assets/{MockForm.c9441864.js → MockForm.0b819a52.js} +2 -2
- abstra_statics/dist/assets/Navbar.ef64aa09.js +2 -0
- abstra_statics/dist/assets/NewEditor.b9c410fe.css +1 -0
- abstra_statics/dist/assets/NewEditor.c928dbe9.js +8 -0
- abstra_statics/dist/assets/OidcLoginCallback.42a7c4f9.js +2 -0
- abstra_statics/dist/assets/OidcLogoutCallback.35931a68.js +2 -0
- abstra_statics/dist/assets/OmniChat.73d9f6bb.js +6 -0
- abstra_statics/dist/assets/OmniChat.a21fec40.css +1 -0
- abstra_statics/dist/assets/{OnboardingView.fbc4b6fe.js → OnboardingView.25042384.js} +2 -2
- abstra_statics/dist/assets/{Organization.bc495099.js → Organization.66f64939.js} +2 -2
- abstra_statics/dist/assets/Organizations.47868beb.js +2 -0
- abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.156bcd89.js → PhArrowCounterClockwise.vue.78877a50.js} +2 -2
- abstra_statics/dist/assets/{PhArrowSquareOut.vue.d0c95a06.js → PhArrowSquareOut.vue.2ddfc219.js} +2 -2
- abstra_statics/dist/assets/{PhBookBookmark.vue.42e49494.js → PhBookBookmark.vue.41108214.js} +2 -2
- abstra_statics/dist/assets/{PhChats.vue.54d692e4.js → PhChats.vue.71e5cb89.js} +2 -2
- abstra_statics/dist/assets/{PhClockCounterClockwise.vue.d47d66ba.js → PhClockCounterClockwise.vue.854c3cb5.js} +2 -2
- abstra_statics/dist/assets/{PhCopy.vue.a0d9b0ec.js → PhCopy.vue.d116ddfb.js} +2 -2
- abstra_statics/dist/assets/{PhCopySimple.vue.43c74ebe.js → PhCopySimple.vue.5d0f839d.js} +2 -2
- abstra_statics/dist/assets/{PhCube.vue.498c014d.js → PhCube.vue.abf4a034.js} +2 -2
- abstra_statics/dist/assets/PhDatabase.vue.a94d95f6.js +2 -0
- abstra_statics/dist/assets/{PhDotsThreeVertical.vue.0ea03d82.js → PhDotsThreeVertical.vue.5e8ae2a9.js} +2 -2
- abstra_statics/dist/assets/{PhDownloadSimple.vue.c92aeaff.js → PhDownloadSimple.vue.e88f64f1.js} +2 -2
- abstra_statics/dist/assets/{PhFolderPlus.vue.0c210f8d.js → PhFolderPlus.vue.21d377a3.js} +2 -2
- abstra_statics/dist/assets/{PhGear.vue.86c3014a.js → PhGear.vue.363dd83c.js} +2 -2
- abstra_statics/dist/assets/{PhKey.vue.72ce23d3.js → PhKey.vue.23f3a465.js} +2 -2
- abstra_statics/dist/assets/{PhPencil.vue.80ed4b2e.js → PhPencil.vue.feb383e2.js} +2 -2
- abstra_statics/dist/assets/{PhPencilSimple.vue.0046d784.js → PhPencilSimple.vue.ec0eebb4.js} +2 -2
- abstra_statics/dist/assets/{PhRocket.vue.f3302a7e.js → PhRocket.vue.261a42b2.js} +2 -2
- abstra_statics/dist/assets/{PhSignOut.vue.c9150da4.js → PhSignOut.vue.d1d6498f.js} +2 -2
- abstra_statics/dist/assets/{PhSparkle.vue.84ea95fc.js → PhSparkle.vue.b6712d34.js} +2 -2
- abstra_statics/dist/assets/{PhUserList.vue.bce47902.js → PhUserList.vue.e0b8c21b.js} +2 -2
- abstra_statics/dist/assets/{PhUsersThree.vue.0a84dfa4.js → PhUsersThree.vue.8889f957.js} +2 -2
- abstra_statics/dist/assets/PhWarningCircle.vue.2087be7d.js +2 -0
- abstra_statics/dist/assets/{PhWebhooksLogo.vue.a704632d.js → PhWebhooksLogo.vue.d164db1e.js} +2 -2
- abstra_statics/dist/assets/{PlayerConfigProvider.26b585a1.js → PlayerConfigProvider.28e29025.js} +2 -2
- abstra_statics/dist/assets/{PlayerNavbar.0fa6f760.js → PlayerNavbar.aaad1dfd.js} +2 -2
- abstra_statics/dist/assets/Project.2de506d7.js +2 -0
- abstra_statics/dist/assets/{ProjectLogin.934271a6.js → ProjectLogin.22c8b093.js} +2 -2
- abstra_statics/dist/assets/{ProjectSettings.2bf7e6c8.js → ProjectSettings.d065c292.js} +2 -2
- abstra_statics/dist/assets/{ProjectsView.ed31b921.js → ProjectsView.cbd118ac.js} +2 -2
- abstra_statics/dist/assets/{SaveButton.fdf70b31.js → SaveButton.428227de.js} +2 -2
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.6d724b05.js +2 -0
- abstra_statics/dist/assets/{Sidebar.e69f49bd.css → Sidebar.29baeab0.css} +1 -1
- abstra_statics/dist/assets/{Sidebar.781afa0a.js → Sidebar.41173d8e.js} +2 -2
- abstra_statics/dist/assets/Sql.90e6e2ba.css +1 -0
- abstra_statics/dist/assets/Sql.c070dd2c.js +5 -0
- abstra_statics/dist/assets/Steps.dd51a108.js +2 -0
- abstra_statics/dist/assets/TableCard.529112b9.css +1 -0
- abstra_statics/dist/assets/TableCard.c9a1cf41.js +2 -0
- abstra_statics/dist/assets/TableEditor.1bc47a95.js +2 -0
- abstra_statics/dist/assets/TableEditor.5853a363.css +1 -0
- abstra_statics/dist/assets/Tables.eff119eb.js +2 -0
- abstra_statics/dist/assets/TablesDiagram.a588e7ff.css +1 -0
- abstra_statics/dist/assets/TablesDiagram.d633acaf.js +15 -0
- abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.cf7e34c5.js +2 -0
- abstra_statics/dist/assets/{Tasks.ee450480.js → Tasks.c311b05f.js} +2 -2
- abstra_statics/dist/assets/{UploadOutlined.e3072945.js → UploadOutlined.9aa845de.js} +2 -2
- abstra_statics/dist/assets/View.dd7ba25a.js +2 -0
- abstra_statics/dist/assets/{View.vue_vue_type_script_setup_true_lang.ee5d447b.js → View.vue_vue_type_script_setup_true_lang.e6563207.js} +2 -2
- abstra_statics/dist/assets/{Watermark.891eee9f.js → Watermark.9c9358e3.js} +2 -2
- abstra_statics/dist/assets/{WebEditor.7409cd48.js → WebEditor.83f2a960.js} +2 -2
- abstra_statics/dist/assets/WidgetPreview.7c6a231c.js +2 -0
- abstra_statics/dist/assets/ant-design.42419021.js +2 -0
- abstra_statics/dist/assets/apiKey.403049eb.js +2 -0
- abstra_statics/dist/assets/asyncComputed.47aec2b5.js +2 -0
- abstra_statics/dist/assets/{build.8fa1a961.js → build.45b511b6.js} +2 -2
- abstra_statics/dist/assets/colorHelpers.50ced1e9.js +2 -0
- abstra_statics/dist/assets/console.f0f09e6f.js +17 -0
- abstra_statics/dist/assets/constants.d569f33d.js +2 -0
- abstra_statics/dist/assets/{contracts.generated.c4057ed0.js → contracts.generated.aefe8d0a.js} +2 -2
- abstra_statics/dist/assets/{cssMode.4c65b876.js → cssMode.23271d4a.js} +2 -2
- abstra_statics/dist/assets/{datetime.e5660676.js → datetime.82608dfb.js} +2 -2
- abstra_statics/dist/assets/dayjs.7f71c60a.js +2 -0
- abstra_statics/dist/assets/editor.02184ced.js +2 -0
- abstra_statics/dist/assets/editor.main.0f931e87.js +2 -0
- abstra_statics/dist/assets/fetch.1b3d01a0.js +2 -0
- abstra_statics/dist/assets/files.8b58ee7e.js +2 -0
- abstra_statics/dist/assets/folder.13d3acac.js +2 -0
- abstra_statics/dist/assets/{freemarker2.1d872d48.js → freemarker2.f79b510b.js} +2 -2
- abstra_statics/dist/assets/{handlebars.41fc6db8.js → handlebars.07d5febe.js} +2 -2
- abstra_statics/dist/assets/{html.967e3c6d.js → html.7d6a59b4.js} +3 -3
- abstra_statics/dist/assets/{htmlMode.d38ab72a.js → htmlMode.7779ff94.js} +2 -2
- abstra_statics/dist/assets/{index.8e871bae.js → index.482df04a.js} +2 -2
- abstra_statics/dist/assets/{index.4b93c8ad.js → index.581e2edd.js} +2 -2
- abstra_statics/dist/assets/index.966ac1b9.js +2 -0
- abstra_statics/dist/assets/{index.9021ba5d.js → index.c7a996a6.js} +2 -2
- abstra_statics/dist/assets/{index.03d222dd.js → index.c8a56795.js} +2 -2
- abstra_statics/dist/assets/{index.5f3f38ed.js → index.d31da2c3.js} +2 -2
- abstra_statics/dist/assets/{index.51dbb698.js → index.d53182ed.js} +2 -2
- abstra_statics/dist/assets/{index.b762f5e8.js → index.e71a5f84.js} +2 -2
- abstra_statics/dist/assets/{javascript.0935bea2.js → javascript.4c528c2c.js} +3 -3
- abstra_statics/dist/assets/{jsonMode.2860b71c.js → jsonMode.d6441e9d.js} +2 -2
- abstra_statics/dist/assets/{jwt-decode.esm.7f2ef0df.js → jwt-decode.esm.54a1ea22.js} +8 -8
- abstra_statics/dist/assets/linters.a0f2aa84.js +2 -0
- abstra_statics/dist/assets/{liquid.1bae5f6b.js → liquid.0c441ad2.js} +2 -2
- abstra_statics/dist/assets/member.2260c37e.js +2 -0
- abstra_statics/dist/assets/{metadata.39f9b9ba.js → metadata.0098e20c.js} +2 -2
- abstra_statics/dist/assets/omniChatStore.dd7a1c46.js +8 -0
- abstra_statics/dist/assets/{organization.a877b653.js → organization.668d1b58.js} +2 -2
- abstra_statics/dist/assets/player.4ab4aed5.js +2 -0
- abstra_statics/dist/assets/{plotly.min.d3f75723.js → plotly.min.2a87d7e2.js} +2 -2
- abstra_statics/dist/assets/polling.b5a32c22.js +2 -0
- abstra_statics/dist/assets/{project.d22a89ee.js → project.1b8374f4.js} +2 -2
- abstra_statics/dist/assets/{python.4c1a1300.js → python.7ac27a56.js} +3 -3
- abstra_statics/dist/assets/{razor.ba8bdb33.js → razor.860a0279.js} +2 -2
- abstra_statics/dist/assets/{record.4ffc477c.js → record.e83a2eb2.js} +2 -2
- abstra_statics/dist/assets/{redirect.ce3c0f65.js → redirect.586749f4.js} +2 -2
- abstra_statics/dist/assets/{repository.360feb8f.js → repository.3a331f0f.js} +2 -2
- abstra_statics/dist/assets/{repository.ab3036a9.js → repository.66c7567c.js} +2 -2
- abstra_statics/dist/assets/router.06ee2b9d.js +2 -0
- abstra_statics/dist/assets/router.4dfadf5d.js +18 -0
- abstra_statics/dist/assets/string.61e11a30.js +2 -0
- abstra_statics/dist/assets/{tables.be1c51f8.js → tables.1bcb1140.js} +2 -2
- abstra_statics/dist/assets/{tasksController.bf12e264.js → tasksController.f3adf725.js} +2 -2
- abstra_statics/dist/assets/{toggleHighContrast.c4e1b24d.js → toggleHighContrast.9535bf54.js} +7 -7
- abstra_statics/dist/assets/{tsMode.da264aae.js → tsMode.c0f9fe1a.js} +2 -2
- abstra_statics/dist/assets/{typescript.db5d7517.js → typescript.310eae6d.js} +3 -3
- abstra_statics/dist/assets/url.f5664225.js +2 -0
- abstra_statics/dist/assets/{useCodebaseEvents.42573b00.js → useCodebaseEvents.276cc8f0.js} +2 -2
- abstra_statics/dist/assets/useTables.18fc3efd.js +2 -0
- abstra_statics/dist/assets/userStore.6ab1e5ac.js +2 -0
- abstra_statics/dist/assets/uuid.5b8ba8af.js +2 -0
- abstra_statics/dist/assets/vue-flow-background.af096866.js +2 -0
- abstra_statics/dist/assets/vue-flow-core.6cb99d32.js +22 -0
- abstra_statics/dist/assets/{vue-quill.esm-bundler.37119951.js → vue-quill.esm-bundler.e3d34d0e.js} +2 -2
- abstra_statics/dist/assets/{workspaceStore.50ef2df1.js → workspaceStore.fac1e9a9.js} +2 -2
- abstra_statics/dist/assets/{xml.94b88503.js → xml.04864db4.js} +3 -3
- abstra_statics/dist/assets/{yaml.73b7d5ce.js → yaml.f30375fe.js} +3 -3
- abstra_statics/dist/console.html +14 -15
- abstra_statics/dist/editor.html +13 -13
- abstra_statics/dist/player.html +9 -9
- tests/e2e/test_crud_files.py +1 -0
- abstra_internals/templates/abstraignore +0 -8
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.3f03a3ef.js +0 -2
- abstra_statics/dist/assets/ApiKeys.cb561e62.js +0 -2
- abstra_statics/dist/assets/App.dc82115f.js +0 -2
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.3640ec1c.js +0 -2
- abstra_statics/dist/assets/ConnectorsView.b428e487.js +0 -2
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.506f28c6.js +0 -2
- abstra_statics/dist/assets/ContentLayout.228d2328.js +0 -2
- abstra_statics/dist/assets/DocsButton.vue_vue_type_script_setup_true_lang.61381525.js +0 -2
- abstra_statics/dist/assets/EnvVars.242b97c2.js +0 -2
- abstra_statics/dist/assets/Error.466dbb94.js +0 -2
- abstra_statics/dist/assets/ExclamationCircleOutlined.6d10f60b.js +0 -2
- abstra_statics/dist/assets/Files.104385dd.js +0 -2
- abstra_statics/dist/assets/Home.80c7e349.js +0 -2
- abstra_statics/dist/assets/LoadingContainer.6e2b63e4.js +0 -2
- abstra_statics/dist/assets/LoadingOutlined.cd84d9c9.js +0 -2
- abstra_statics/dist/assets/Login.409f4a11.js +0 -2
- abstra_statics/dist/assets/Logo.2de02c4a.js +0 -2
- abstra_statics/dist/assets/Main.4370ed68.js +0 -2
- abstra_statics/dist/assets/Navbar.07ba9452.js +0 -2
- abstra_statics/dist/assets/NewEditor.769f4459.js +0 -8
- abstra_statics/dist/assets/NewEditor.d6e41a05.css +0 -1
- abstra_statics/dist/assets/OidcLoginCallback.a89857fe.js +0 -2
- abstra_statics/dist/assets/OidcLogoutCallback.d151c695.js +0 -2
- abstra_statics/dist/assets/OmniChat.0f64dfec.css +0 -1
- abstra_statics/dist/assets/OmniChat.d015bfa8.js +0 -6
- abstra_statics/dist/assets/Organizations.b630803f.js +0 -2
- abstra_statics/dist/assets/PhPencilSimpleLine.vue.34633dfa.js +0 -2
- abstra_statics/dist/assets/Project.c03610d3.js +0 -2
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.3bbea426.js +0 -2
- abstra_statics/dist/assets/Sql.3cdc910a.css +0 -1
- abstra_statics/dist/assets/Sql.b6aa38ca.js +0 -5
- abstra_statics/dist/assets/Steps.7c7e4a4a.js +0 -2
- abstra_statics/dist/assets/TableEditor.1e680eaf.css +0 -1
- abstra_statics/dist/assets/TableEditor.dc1b4a2d.js +0 -2
- abstra_statics/dist/assets/Tables.de30953b.js +0 -2
- abstra_statics/dist/assets/TablesDiagram.1ec45dd9.css +0 -1
- abstra_statics/dist/assets/TablesDiagram.97d6a43f.js +0 -15
- abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.63aa07d0.js +0 -2
- abstra_statics/dist/assets/View.617ad8d8.js +0 -2
- abstra_statics/dist/assets/WidgetPreview.99f14714.js +0 -2
- abstra_statics/dist/assets/ant-design.4952c8fb.js +0 -2
- abstra_statics/dist/assets/apiKey.864dc66b.js +0 -2
- abstra_statics/dist/assets/asyncComputed.25309626.js +0 -2
- abstra_statics/dist/assets/colorHelpers.71d6d61d.js +0 -2
- abstra_statics/dist/assets/console.3d4702c3.js +0 -25
- abstra_statics/dist/assets/constants.56e8988f.js +0 -2
- abstra_statics/dist/assets/dayjs.c54f8edb.js +0 -2
- abstra_statics/dist/assets/editor.6d0baf6f.js +0 -2
- abstra_statics/dist/assets/editor.main.65812c73.js +0 -2
- abstra_statics/dist/assets/fetch.5136a62d.js +0 -2
- abstra_statics/dist/assets/folder.d7d65e5b.js +0 -2
- abstra_statics/dist/assets/index.23283fbb.js +0 -2
- abstra_statics/dist/assets/index.58e29274.js +0 -2
- abstra_statics/dist/assets/linters.640d6098.js +0 -2
- abstra_statics/dist/assets/member.b7ac8000.js +0 -2
- abstra_statics/dist/assets/omniChatStore.3431c026.js +0 -8
- abstra_statics/dist/assets/player.30593e18.js +0 -2
- abstra_statics/dist/assets/polling.d1c934c7.js +0 -2
- abstra_statics/dist/assets/router.7f571832.js +0 -2
- abstra_statics/dist/assets/router.8fd5b2ad.js +0 -10
- abstra_statics/dist/assets/string.2ed1cde3.js +0 -2
- abstra_statics/dist/assets/url.8583a595.js +0 -2
- abstra_statics/dist/assets/userStore.73b89fbb.js +0 -2
- abstra_statics/dist/assets/uuid.dadede91.js +0 -2
- abstra_statics/dist/assets/vue-flow-background.d2772d9a.js +0 -22
- {abstra-3.23.12.dist-info → abstra-3.24.1.dist-info}/WHEEL +0 -0
- {abstra-3.23.12.dist-info → abstra-3.24.1.dist-info}/entry_points.txt +0 -0
- {abstra-3.23.12.dist-info → abstra-3.24.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from abstra_internals.environment import REMOTE_NAME
|
|
7
|
+
|
|
8
|
+
from .types import GitCommit, GitFileChange, GitRepositoryInterface, GitStatus
|
|
9
|
+
|
|
10
|
+
TEMP_ABSTRA_EMAIL = "abstra@abstra.app"
|
|
11
|
+
TEMP_ABSTRA_NAME = "Abstra"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NativeGitRepository(GitRepositoryInterface):
|
|
15
|
+
"""Repository for Git operations using system git command"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, working_directory: Path):
|
|
18
|
+
super().__init__(working_directory)
|
|
19
|
+
self._git_available = None
|
|
20
|
+
|
|
21
|
+
def _run_git_command(
|
|
22
|
+
self, command: List[str], cwd: Optional[Path] = None
|
|
23
|
+
) -> Tuple[bool, str, str]:
|
|
24
|
+
"""Run a git command and return success, stdout, stderr"""
|
|
25
|
+
try:
|
|
26
|
+
if cwd is None:
|
|
27
|
+
cwd = self.working_directory
|
|
28
|
+
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git"] + command, cwd=cwd, capture_output=True, text=True, timeout=30
|
|
31
|
+
)
|
|
32
|
+
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
|
33
|
+
except (
|
|
34
|
+
subprocess.TimeoutExpired,
|
|
35
|
+
subprocess.CalledProcessError,
|
|
36
|
+
FileNotFoundError,
|
|
37
|
+
) as e:
|
|
38
|
+
return False, "", str(e)
|
|
39
|
+
|
|
40
|
+
def is_git_available(self) -> bool:
|
|
41
|
+
"""Check if git is installed and available"""
|
|
42
|
+
if self._git_available is None:
|
|
43
|
+
self._git_available = shutil.which("git") is not None
|
|
44
|
+
return self._git_available
|
|
45
|
+
|
|
46
|
+
def find_git_root(self, start_path: Optional[Path] = None) -> Optional[Path]:
|
|
47
|
+
"""Find the root directory of the git repository"""
|
|
48
|
+
if not self.is_git_available():
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
if start_path is None:
|
|
52
|
+
start_path = self.working_directory
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
success, stdout, _ = self._run_git_command(
|
|
56
|
+
["rev-parse", "--show-toplevel"], cwd=start_path
|
|
57
|
+
)
|
|
58
|
+
if success and stdout:
|
|
59
|
+
return Path(stdout.strip()).resolve()
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def is_git_repository(self) -> bool:
|
|
64
|
+
"""Check if current directory is a git repository"""
|
|
65
|
+
if not self.is_git_available():
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
success, _, _ = self._run_git_command(["rev-parse", "--git-dir"])
|
|
69
|
+
return success
|
|
70
|
+
|
|
71
|
+
def get_current_branch(self) -> Optional[str]:
|
|
72
|
+
"""Get the current branch name"""
|
|
73
|
+
if not self.is_git_repository():
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
success, stdout, _ = self._run_git_command(["branch", "--show-current"])
|
|
77
|
+
if success and stdout:
|
|
78
|
+
return stdout
|
|
79
|
+
|
|
80
|
+
success, stdout, _ = self._run_git_command(["symbolic-ref", "--short", "HEAD"])
|
|
81
|
+
if success and stdout:
|
|
82
|
+
return stdout
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def get_all_branches(self) -> List[str]:
|
|
87
|
+
"""Get all local branches"""
|
|
88
|
+
if not self.is_git_repository():
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
success, stdout, _ = self._run_git_command(
|
|
92
|
+
["branch", "--format=%(refname:short)"]
|
|
93
|
+
)
|
|
94
|
+
if success and stdout:
|
|
95
|
+
return [branch.strip() for branch in stdout.split("\n") if branch.strip()]
|
|
96
|
+
|
|
97
|
+
# Fallback for repositories without commits: check if HEAD points to a branch
|
|
98
|
+
# This handles the case where a repository was initialized but no commits were made yet
|
|
99
|
+
head_success, head_output, _ = self._run_git_command(
|
|
100
|
+
["symbolic-ref", "--short", "HEAD"]
|
|
101
|
+
)
|
|
102
|
+
if head_success and head_output.strip():
|
|
103
|
+
return [head_output.strip()]
|
|
104
|
+
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
def get_last_commit(self) -> Optional[GitCommit]:
|
|
108
|
+
"""Get information about the last commit"""
|
|
109
|
+
commits = self.get_commit_history(limit=1)
|
|
110
|
+
return commits[0] if commits else None
|
|
111
|
+
|
|
112
|
+
def get_commit_history(
|
|
113
|
+
self, limit: int = 10, offset: int = 0, branch: Optional[str] = None
|
|
114
|
+
) -> List[GitCommit]:
|
|
115
|
+
"""Get commit history with pagination"""
|
|
116
|
+
if not self.is_git_repository():
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
if branch:
|
|
120
|
+
git_args = ["log", branch]
|
|
121
|
+
else:
|
|
122
|
+
git_args = ["log", "--all"]
|
|
123
|
+
|
|
124
|
+
git_args.extend(
|
|
125
|
+
[
|
|
126
|
+
f"--skip={offset}",
|
|
127
|
+
f"--max-count={limit}",
|
|
128
|
+
"--format=%H|%s|%an|%ad",
|
|
129
|
+
"--date=iso",
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
success, stdout, _ = self._run_git_command(git_args)
|
|
134
|
+
|
|
135
|
+
if success and stdout:
|
|
136
|
+
commits = []
|
|
137
|
+
for line in stdout.strip().split("\n"):
|
|
138
|
+
if line.strip():
|
|
139
|
+
parts = line.split("|", 3)
|
|
140
|
+
if len(parts) == 4:
|
|
141
|
+
commits.append(
|
|
142
|
+
GitCommit(
|
|
143
|
+
hash=parts[0],
|
|
144
|
+
message=parts[1],
|
|
145
|
+
author=parts[2],
|
|
146
|
+
date=parts[3],
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
return commits
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
def get_changed_files(self) -> List[str]:
|
|
153
|
+
"""Get list of changed files"""
|
|
154
|
+
if not self.is_git_repository():
|
|
155
|
+
return []
|
|
156
|
+
|
|
157
|
+
success, stdout, _ = self._run_git_command(["status", "--porcelain"])
|
|
158
|
+
if success and stdout:
|
|
159
|
+
files = []
|
|
160
|
+
for line in stdout.split("\n"):
|
|
161
|
+
if line.strip():
|
|
162
|
+
filename = line[2:].lstrip() if len(line) > 2 else ""
|
|
163
|
+
if filename:
|
|
164
|
+
files.append(filename)
|
|
165
|
+
return files
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
def get_changed_files_with_status(self) -> List[GitFileChange]:
|
|
169
|
+
"""Get list of changed files with their git status"""
|
|
170
|
+
if not self.is_git_repository():
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
success, stdout, _ = self._run_git_command(["status", "--porcelain"])
|
|
174
|
+
if success and stdout:
|
|
175
|
+
files = []
|
|
176
|
+
for line in stdout.split("\n"):
|
|
177
|
+
if line.strip():
|
|
178
|
+
if len(line) < 3:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
status_code = line[:2]
|
|
182
|
+
filename = line[2:].lstrip() if len(line) > 2 else ""
|
|
183
|
+
|
|
184
|
+
if not filename:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if status_code[0] == "A" or status_code[1] == "A":
|
|
188
|
+
status = "added"
|
|
189
|
+
elif status_code[0] == "D" or status_code[1] == "D":
|
|
190
|
+
status = "deleted"
|
|
191
|
+
elif status_code[0] == "M" or status_code[1] == "M":
|
|
192
|
+
status = "modified"
|
|
193
|
+
elif status_code[0] == "R" or status_code[1] == "R":
|
|
194
|
+
status = "renamed"
|
|
195
|
+
elif status_code[0] == "?" and status_code[1] == "?":
|
|
196
|
+
status = "untracked"
|
|
197
|
+
else:
|
|
198
|
+
status = "modified"
|
|
199
|
+
|
|
200
|
+
files.append(
|
|
201
|
+
GitFileChange(
|
|
202
|
+
path=filename, status=status, status_code=status_code
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return files
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
def has_uncommitted_changes(self) -> bool:
|
|
209
|
+
"""Check if there are uncommitted changes"""
|
|
210
|
+
return len(self.get_changed_files()) > 0
|
|
211
|
+
|
|
212
|
+
def init_repository(self) -> bool:
|
|
213
|
+
"""Initialize a new git repository in the working directory"""
|
|
214
|
+
if not self.is_git_available():
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Initialize repository with main as default branch
|
|
218
|
+
success, _, _ = self._run_git_command(["init", "-b", "main"])
|
|
219
|
+
if not success:
|
|
220
|
+
# Fallback for older git versions that don't support -b flag
|
|
221
|
+
success, _, _ = self._run_git_command(["init"])
|
|
222
|
+
if not success:
|
|
223
|
+
return False
|
|
224
|
+
# Set main as default branch
|
|
225
|
+
self._run_git_command(["symbolic-ref", "HEAD", "refs/heads/main"])
|
|
226
|
+
|
|
227
|
+
self.configure_git_user(TEMP_ABSTRA_EMAIL, TEMP_ABSTRA_NAME)
|
|
228
|
+
|
|
229
|
+
add_success, _, _ = self._run_git_command(["add", "."])
|
|
230
|
+
if add_success:
|
|
231
|
+
status_success, status_output, _ = self._run_git_command(
|
|
232
|
+
["status", "--porcelain", "--cached"]
|
|
233
|
+
)
|
|
234
|
+
if status_success and status_output.strip():
|
|
235
|
+
commit_success, _, _ = self._run_git_command(
|
|
236
|
+
["commit", "-m", "First commit"]
|
|
237
|
+
)
|
|
238
|
+
if commit_success:
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# If no files to commit or commit failed, create an empty initial commit
|
|
242
|
+
# This ensures we have a branch with at least one commit
|
|
243
|
+
empty_commit_success, err, out = self._run_git_command(
|
|
244
|
+
["commit", "--allow-empty", "-m", "First commit"]
|
|
245
|
+
)
|
|
246
|
+
print("Created empty initial commit AAAA.", empty_commit_success, err, out)
|
|
247
|
+
return empty_commit_success
|
|
248
|
+
|
|
249
|
+
def configure_git_user(self, fallback_email: str, fallback_name: str):
|
|
250
|
+
"""Ensure git user is configured for commits (required in CI environments)"""
|
|
251
|
+
name_success, name_stdout, _ = self._run_git_command(["config", "user.name"])
|
|
252
|
+
if not name_success or name_stdout == TEMP_ABSTRA_NAME:
|
|
253
|
+
self._run_git_command(["config", "user.name", fallback_name])
|
|
254
|
+
|
|
255
|
+
email_success, email_stdout, _ = self._run_git_command(["config", "user.email"])
|
|
256
|
+
if not email_success or email_stdout == TEMP_ABSTRA_EMAIL:
|
|
257
|
+
self._run_git_command(["config", "user.email", fallback_email])
|
|
258
|
+
|
|
259
|
+
def get_repository_status(self) -> GitStatus:
|
|
260
|
+
"""Get comprehensive repository status"""
|
|
261
|
+
if not self.is_git_available():
|
|
262
|
+
return GitStatus(available=False, git_installed=False)
|
|
263
|
+
|
|
264
|
+
git_root = self.find_git_root()
|
|
265
|
+
if (
|
|
266
|
+
git_root is not None
|
|
267
|
+
and git_root.resolve() != self.working_directory.resolve()
|
|
268
|
+
):
|
|
269
|
+
return GitStatus(available=False, is_monorepo=True)
|
|
270
|
+
|
|
271
|
+
if self.is_git_repository():
|
|
272
|
+
git_root = self.working_directory
|
|
273
|
+
original_working_dir = None
|
|
274
|
+
elif git_root := self.find_git_root():
|
|
275
|
+
original_working_dir = self.working_directory
|
|
276
|
+
self.working_directory = git_root
|
|
277
|
+
elif self.init_repository():
|
|
278
|
+
git_root = self.working_directory
|
|
279
|
+
original_working_dir = None
|
|
280
|
+
else:
|
|
281
|
+
return GitStatus(available=False)
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
branch = self.get_current_branch()
|
|
285
|
+
if not branch:
|
|
286
|
+
success, stdout, _ = self._run_git_command(["rev-parse", "HEAD"])
|
|
287
|
+
if success and stdout:
|
|
288
|
+
branch = f"detached-{stdout[:8]}"
|
|
289
|
+
else:
|
|
290
|
+
return GitStatus(available=False)
|
|
291
|
+
|
|
292
|
+
branches = self.get_all_branches()
|
|
293
|
+
last_commit = self.get_last_commit()
|
|
294
|
+
changed_files = self.get_changed_files()
|
|
295
|
+
changed_files_with_status = self.get_changed_files_with_status()
|
|
296
|
+
has_changes = len(changed_files) > 0
|
|
297
|
+
|
|
298
|
+
return GitStatus(
|
|
299
|
+
available=True,
|
|
300
|
+
branch=branch,
|
|
301
|
+
branches=branches,
|
|
302
|
+
last_commit=last_commit,
|
|
303
|
+
has_changes=has_changes,
|
|
304
|
+
changed_files=changed_files,
|
|
305
|
+
changed_files_with_status=changed_files_with_status,
|
|
306
|
+
)
|
|
307
|
+
finally:
|
|
308
|
+
if "original_working_dir" in locals() and original_working_dir is not None:
|
|
309
|
+
self.working_directory = original_working_dir
|
|
310
|
+
|
|
311
|
+
def checkout_branch(self, branch_name: str) -> bool:
|
|
312
|
+
"""Switch to a different branch"""
|
|
313
|
+
if not self.is_git_repository():
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
success, _, stderr = self._run_git_command(["checkout", branch_name])
|
|
317
|
+
if not success and "did not match any file(s) known to git" in stderr:
|
|
318
|
+
success, _, _ = self._run_git_command(
|
|
319
|
+
["checkout", "-b", branch_name, f"origin/{branch_name}"]
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return success
|
|
323
|
+
|
|
324
|
+
def checkout_commit(self, commit_hash: str) -> bool:
|
|
325
|
+
"""Switch to a specific commit (detached HEAD state)"""
|
|
326
|
+
if not self.is_git_repository():
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
success, _, _ = self._run_git_command(["checkout", commit_hash])
|
|
330
|
+
return success
|
|
331
|
+
|
|
332
|
+
def pull_changes(
|
|
333
|
+
self,
|
|
334
|
+
strategy: str = "merge",
|
|
335
|
+
allow_unrelated: bool = True,
|
|
336
|
+
conflict_resolution: Optional[str] = "theirs",
|
|
337
|
+
) -> bool:
|
|
338
|
+
"""Pull changes from abstra remote repository
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
strategy: How to reconcile divergent branches. Options:
|
|
342
|
+
- "merge": Create a merge commit (git pull --no-rebase)
|
|
343
|
+
- "rebase": Rebase local commits on top of remote (git pull --rebase)
|
|
344
|
+
- "ff-only": Only allow fast-forward merges (git pull --ff-only)
|
|
345
|
+
allow_unrelated: Allow merging unrelated histories (git pull --allow-unrelated-histories)
|
|
346
|
+
conflict_resolution: How to resolve conflicts automatically. Options:
|
|
347
|
+
- "theirs": Accept all incoming changes (git pull -X theirs)
|
|
348
|
+
- "ours": Keep all local changes (git pull -X ours)
|
|
349
|
+
- None: Handle conflicts manually (default)
|
|
350
|
+
"""
|
|
351
|
+
if not self.is_git_repository():
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
pull_cmd = ["pull"]
|
|
355
|
+
|
|
356
|
+
if strategy == "merge":
|
|
357
|
+
pull_cmd.append("--no-rebase")
|
|
358
|
+
elif strategy == "rebase":
|
|
359
|
+
pull_cmd.append("--rebase")
|
|
360
|
+
elif strategy == "ff-only":
|
|
361
|
+
pull_cmd.append("--ff-only")
|
|
362
|
+
else:
|
|
363
|
+
pull_cmd.append("--no-rebase")
|
|
364
|
+
|
|
365
|
+
if allow_unrelated:
|
|
366
|
+
pull_cmd.append("--allow-unrelated-histories")
|
|
367
|
+
|
|
368
|
+
if conflict_resolution == "theirs":
|
|
369
|
+
pull_cmd.extend(["-X", "theirs"])
|
|
370
|
+
elif conflict_resolution == "ours":
|
|
371
|
+
pull_cmd.extend(["-X", "ours"])
|
|
372
|
+
|
|
373
|
+
pull_cmd.extend([REMOTE_NAME, "main"])
|
|
374
|
+
|
|
375
|
+
success, _, e = self._run_git_command(pull_cmd)
|
|
376
|
+
if not success:
|
|
377
|
+
print("There was an error during git pull:", e)
|
|
378
|
+
|
|
379
|
+
return success
|
|
380
|
+
|
|
381
|
+
def commit_changes(self, message: str, add_all: bool = True) -> bool:
|
|
382
|
+
"""Commit changes with a message"""
|
|
383
|
+
if not self.is_git_repository():
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
if add_all:
|
|
387
|
+
success, _, _ = self._run_git_command(["add", "."])
|
|
388
|
+
if not success:
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
success, _, _ = self._run_git_command(["commit", "-m", message])
|
|
392
|
+
return success
|
|
393
|
+
|
|
394
|
+
def stash_changes(self, message: str = "WIP") -> bool:
|
|
395
|
+
"""Stash uncommitted changes"""
|
|
396
|
+
if not self.is_git_repository():
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
success, _, _ = self._run_git_command(
|
|
400
|
+
["stash", "push", "-m", message, "--include-untracked"]
|
|
401
|
+
)
|
|
402
|
+
return success
|
|
403
|
+
|
|
404
|
+
def get_remotes(self) -> List[str]:
|
|
405
|
+
"""Get list of remote names"""
|
|
406
|
+
if not self.is_git_repository():
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
success, stdout, _ = self._run_git_command(["remote"])
|
|
410
|
+
if success and stdout:
|
|
411
|
+
return [remote.strip() for remote in stdout.split("\n") if remote.strip()]
|
|
412
|
+
return []
|
|
413
|
+
|
|
414
|
+
def has_remote(self, remote_name: str) -> bool:
|
|
415
|
+
"""Check if a remote exists"""
|
|
416
|
+
return remote_name in self.get_remotes()
|
|
417
|
+
|
|
418
|
+
def add_remote(self, remote_name: str, remote_url: str) -> bool:
|
|
419
|
+
"""Add a remote to the repository"""
|
|
420
|
+
if not self.is_git_repository():
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
success, _, _ = self._run_git_command(
|
|
424
|
+
["remote", "add", remote_name, remote_url]
|
|
425
|
+
)
|
|
426
|
+
return success
|
|
427
|
+
|
|
428
|
+
def set_remote_url(self, remote_name: str, remote_url: str) -> bool:
|
|
429
|
+
"""Set/update the URL for a remote"""
|
|
430
|
+
if not self.is_git_repository():
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
success, _, _ = self._run_git_command(
|
|
434
|
+
["remote", "set-url", remote_name, remote_url]
|
|
435
|
+
)
|
|
436
|
+
return success
|
|
437
|
+
|
|
438
|
+
def set_git_config(self, key: str, value: str, local: bool = True) -> bool:
|
|
439
|
+
"""Set a git configuration value"""
|
|
440
|
+
if not self.is_git_repository():
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
config_flag = "--local" if local else "--global"
|
|
444
|
+
success, _, _ = self._run_git_command(["config", config_flag, key, value])
|
|
445
|
+
return success
|
|
446
|
+
|
|
447
|
+
def get_git_config(self, key: str, local: bool = True) -> Optional[str]:
|
|
448
|
+
"""Get a git configuration value"""
|
|
449
|
+
if not self.is_git_repository():
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
config_flag = "--local" if local else "--global"
|
|
453
|
+
success, stdout, _ = self._run_git_command(["config", config_flag, key])
|
|
454
|
+
return stdout.strip() if success and stdout else None
|
|
455
|
+
|
|
456
|
+
def get_ahead_behind_count(
|
|
457
|
+
self, local_commit: str, remote_commit: str
|
|
458
|
+
) -> Tuple[int, int]:
|
|
459
|
+
if not self.is_git_repository():
|
|
460
|
+
return (0, 0)
|
|
461
|
+
|
|
462
|
+
remote_exists_success, _, _ = self._run_git_command(
|
|
463
|
+
["cat-file", "-e", remote_commit]
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# If remote commit doesn't exist locally, we need to fetch first (we're behind)
|
|
467
|
+
if not remote_exists_success:
|
|
468
|
+
# We can't calculate exact counts without fetching, but we know:
|
|
469
|
+
# - We have local changes (since commits are different)
|
|
470
|
+
# - Remote has changes we don't have (since remote commit doesn't exist locally)
|
|
471
|
+
# This indicates divergent branches - both ahead and behind
|
|
472
|
+
return (1, 1) # Simplified: indicates divergence requiring merge/rebase
|
|
473
|
+
|
|
474
|
+
# If remote commit exists locally, we can calculate normal ahead/behind
|
|
475
|
+
# Count commits that local has but remote doesn't (ahead)
|
|
476
|
+
ahead_success, ahead_stdout, _ = self._run_git_command(
|
|
477
|
+
["rev-list", "--count", f"{remote_commit}..{local_commit}"]
|
|
478
|
+
)
|
|
479
|
+
ahead_count = (
|
|
480
|
+
int(ahead_stdout.strip()) if ahead_success and ahead_stdout.strip() else 0
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Count commits that remote has but local doesn't (behind)
|
|
484
|
+
behind_success, behind_stdout, _ = self._run_git_command(
|
|
485
|
+
["rev-list", "--count", f"{local_commit}..{remote_commit}"]
|
|
486
|
+
)
|
|
487
|
+
behind_count = (
|
|
488
|
+
int(behind_stdout.strip())
|
|
489
|
+
if behind_success and behind_stdout.strip()
|
|
490
|
+
else 0
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
return (ahead_count, behind_count)
|
|
494
|
+
|
|
495
|
+
def push_and_deploy(self, branch: str = "main") -> bool:
|
|
496
|
+
"""Deploy to Abstra remote (push to abstra remote)"""
|
|
497
|
+
if not self.is_git_repository():
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
if not self.has_remote(REMOTE_NAME):
|
|
501
|
+
return False
|
|
502
|
+
|
|
503
|
+
success, _, _ = self._run_git_command(["push", REMOTE_NAME, branch])
|
|
504
|
+
|
|
505
|
+
return success
|
|
506
|
+
|
|
507
|
+
def revert_commit(self, commit_hash: str) -> bool:
|
|
508
|
+
"""Reset working directory to match a previous commit and create a new commit with that content"""
|
|
509
|
+
if not self.is_git_repository():
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
if not commit_hash.strip():
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
if self.has_uncommitted_changes():
|
|
516
|
+
return False
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
# Step 1: Reset the working directory and index to match the target commit
|
|
520
|
+
# This will replace all files with their state from the target commit
|
|
521
|
+
success_reset, _, _ = self._run_git_command(
|
|
522
|
+
["reset", "--hard", commit_hash]
|
|
523
|
+
)
|
|
524
|
+
if not success_reset:
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
# Step 2: Reset HEAD back to the original position (soft reset)
|
|
528
|
+
# This keeps the files from target commit but moves HEAD back
|
|
529
|
+
success_soft, _, _ = self._run_git_command(["reset", "--soft", "HEAD@{1}"])
|
|
530
|
+
if not success_soft:
|
|
531
|
+
return False
|
|
532
|
+
|
|
533
|
+
# Step 3: Create a new commit with the restored content
|
|
534
|
+
commit_message = f"Restore content from commit {commit_hash[:8]}"
|
|
535
|
+
success_commit, _, _ = self._run_git_command(
|
|
536
|
+
["commit", "-m", commit_message]
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return success_commit
|
|
540
|
+
|
|
541
|
+
except Exception:
|
|
542
|
+
self._run_git_command(["reset", "--hard", "HEAD"])
|
|
543
|
+
return False
|
|
544
|
+
|
|
545
|
+
def check_merge_conflicts(self, remote_commit: str) -> bool:
|
|
546
|
+
"""Check if merging with remote commit would cause conflicts"""
|
|
547
|
+
if not self.is_git_repository():
|
|
548
|
+
return False
|
|
549
|
+
|
|
550
|
+
if not remote_commit:
|
|
551
|
+
return False
|
|
552
|
+
|
|
553
|
+
# Get current commit
|
|
554
|
+
success, current_commit, _ = self._run_git_command(["rev-parse", "HEAD"])
|
|
555
|
+
if not success:
|
|
556
|
+
return False
|
|
557
|
+
|
|
558
|
+
# Use git merge-tree to detect potential conflicts without actually merging
|
|
559
|
+
# git merge-tree <base> <branch1> <branch2>
|
|
560
|
+
# We need to find the merge base first
|
|
561
|
+
success, merge_base, _ = self._run_git_command(
|
|
562
|
+
["merge-base", current_commit.strip(), remote_commit]
|
|
563
|
+
)
|
|
564
|
+
if not success:
|
|
565
|
+
# If no merge base, assume potential conflicts
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
# Check if merge would have conflicts
|
|
569
|
+
success, output, _ = self._run_git_command(
|
|
570
|
+
["merge-tree", merge_base.strip(), current_commit.strip(), remote_commit]
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# If merge-tree succeeds and output contains conflict markers, there are conflicts
|
|
574
|
+
if success and output:
|
|
575
|
+
# Look for conflict markers in the output
|
|
576
|
+
return "<<<<<<< " in output or "=======" in output or ">>>>>>> " in output
|
|
577
|
+
|
|
578
|
+
return False
|