abstra 3.24.0__py3-none-any.whl → 3.24.2__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 +8 -2
- {abstra-3.24.0.dist-info → abstra-3.24.2.dist-info}/METADATA +1 -2
- {abstra-3.24.0.dist-info → abstra-3.24.2.dist-info}/RECORD +205 -209
- abstra_internals/cloud_api/__init__.py +17 -8
- abstra_internals/contracts_generated.py +975 -707
- abstra_internals/controllers/ai.py +89 -1
- abstra_internals/controllers/git.py +6 -2
- abstra_internals/controllers/main.py +1 -1
- abstra_internals/interface/cli/editor.py +2 -2
- abstra_internals/repositories/git/__init__.py +1 -14
- abstra_internals/repositories/git/git_test.py +9 -1216
- abstra_internals/repositories/git/native.py +13 -7
- abstra_internals/repositories/git/types.py +7 -1
- abstra_internals/server/routes/git.py +11 -2
- abstra_statics/dist/assets/AbstraButton.vue_vue_type_script_setup_true_lang.a3ba2a31.js +2 -0
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.b1a71740.js +2 -0
- abstra_statics/dist/assets/ApiKeys.dafa1dc2.js +2 -0
- abstra_statics/dist/assets/App.45396b70.js +2 -0
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.db5596a1.js +2 -0
- abstra_statics/dist/assets/BaseLayout.135a51d9.js +2 -0
- abstra_statics/dist/assets/Billing.622c9155.js +2 -0
- abstra_statics/dist/assets/{Breadcrumb.26dec5f7.js → Breadcrumb.8a82fa01.js} +3 -3
- abstra_statics/dist/assets/Builds.38c0f966.css +1 -0
- abstra_statics/dist/assets/Builds.c3fd9633.js +2 -0
- abstra_statics/dist/assets/{Card.6cfffb80.js → Card.38a860a4.js} +5 -5
- abstra_statics/dist/assets/{CircularLoading.a13d6a76.js → CircularLoading.217756fb.js} +2 -2
- abstra_statics/dist/assets/CloseCircleOutlined.4c4707d8.js +2 -0
- abstra_statics/dist/assets/ConnectorsView.121898a9.js +2 -0
- abstra_statics/dist/assets/ConnectorsView.b594798e.css +1 -0
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.502d779a.js +2 -0
- abstra_statics/dist/assets/ContentLayout.b1c94c2b.js +2 -0
- abstra_statics/dist/assets/CrudView.75a430a4.js +2 -0
- abstra_statics/dist/assets/CrudView.8abe5bc2.css +1 -0
- abstra_statics/dist/assets/DocsButton.vue_vue_type_script_setup_true_lang.a9c8118b.js +2 -0
- abstra_statics/dist/assets/EditorLogin.fba78ce9.js +2 -0
- abstra_statics/dist/assets/EditorsView.1a9ccb13.js +2 -0
- abstra_statics/dist/assets/EnvVars.d77957ea.js +2 -0
- abstra_statics/dist/assets/Error.5bd293cc.js +2 -0
- abstra_statics/dist/assets/ExclamationCircleOutlined.5d5c3f30.js +2 -0
- abstra_statics/dist/assets/Files.39b07d88.css +1 -0
- abstra_statics/dist/assets/Files.c4ce443f.js +2 -0
- abstra_statics/dist/assets/Form.a4787001.js +2 -0
- abstra_statics/dist/assets/{FormRunner.46f6426d.css → FormRunner.1e6e7d2a.css} +1 -1
- abstra_statics/dist/assets/FormRunner.73650f9e.js +2 -0
- abstra_statics/dist/assets/Home.19a2303b.js +2 -0
- abstra_statics/dist/assets/Home.4eff6ce9.js +2 -0
- abstra_statics/dist/assets/Home.e8bf9440.css +1 -0
- abstra_statics/dist/assets/LoadingContainer.97fa8f2e.js +2 -0
- abstra_statics/dist/assets/LoadingOutlined.e309ab16.js +2 -0
- abstra_statics/dist/assets/Login.632cada3.js +2 -0
- abstra_statics/dist/assets/{Login.75d13f6c.css → Login.ac75228f.css} +1 -1
- abstra_statics/dist/assets/Login.ecec1ff2.js +2 -0
- abstra_statics/dist/assets/Login.vue_vue_type_script_setup_true_lang.ada1c6c9.js +2 -0
- abstra_statics/dist/assets/Logo.d77d5637.js +2 -0
- abstra_statics/dist/assets/Logs.c1f01b05.js +2 -0
- abstra_statics/dist/assets/LogsController.0ff97ed4.css +1 -0
- abstra_statics/dist/assets/LogsController.2dceb3d3.js +2 -0
- abstra_statics/dist/assets/Main.44b7640e.js +2 -0
- abstra_statics/dist/assets/{MockForm.deda9355.js → MockForm.9b7a0df3.js} +2 -2
- abstra_statics/dist/assets/Navbar.0951ed6d.js +2 -0
- abstra_statics/dist/assets/Navbar.61d3e0a6.css +1 -0
- abstra_statics/dist/assets/NewEditor.d65a400f.js +8 -0
- abstra_statics/dist/assets/NewEditor.e3cfeb2c.css +1 -0
- abstra_statics/dist/assets/OidcLoginCallback.66b0f38a.js +2 -0
- abstra_statics/dist/assets/OidcLogoutCallback.48d8429a.js +2 -0
- abstra_statics/dist/assets/OmniChat.60ee26c8.css +1 -0
- abstra_statics/dist/assets/OmniChat.c3de8733.js +6 -0
- abstra_statics/dist/assets/OnboardingView.6cda1bc5.js +2 -0
- abstra_statics/dist/assets/OnboardingView.e871e6d8.css +1 -0
- abstra_statics/dist/assets/Organization.c36206b7.js +2 -0
- abstra_statics/dist/assets/Organizations.2b1c6c65.js +2 -0
- abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.8090d021.js → PhArrowCounterClockwise.vue.9e570570.js} +2 -2
- abstra_statics/dist/assets/{PhArrowSquareOut.vue.26582195.js → PhArrowSquareOut.vue.bcbdb6e7.js} +2 -2
- abstra_statics/dist/assets/{PhClockCounterClockwise.vue.812311ad.js → PhClockCounterClockwise.vue.4bd682d8.js} +2 -2
- abstra_statics/dist/assets/{PhCopy.vue.59b0f1b4.js → PhCopy.vue.29934bc2.js} +2 -2
- abstra_statics/dist/assets/PhCopySimple.vue.0241af8c.js +2 -0
- abstra_statics/dist/assets/{PhCube.vue.63ae7d32.js → PhCube.vue.0fe2c514.js} +2 -2
- abstra_statics/dist/assets/PhDatabase.vue.fdfb515c.js +2 -0
- abstra_statics/dist/assets/{PhDotsThreeVertical.vue.ab4580a5.js → PhDotsThreeVertical.vue.7a0e0638.js} +2 -2
- abstra_statics/dist/assets/{PhDownloadSimple.vue.c2eaaad1.js → PhDownloadSimple.vue.f1245c40.js} +2 -2
- abstra_statics/dist/assets/PhFileArrowUp.vue.c292afe1.js +2 -0
- abstra_statics/dist/assets/PhFilePlus.vue.c39ff1a9.js +2 -0
- abstra_statics/dist/assets/{PhFolderPlus.vue.05ba4a5c.js → PhFolderPlus.vue.bc40161e.js} +2 -2
- abstra_statics/dist/assets/{PhGear.vue.0e4a6135.js → PhGear.vue.0feed515.js} +2 -2
- abstra_statics/dist/assets/{PhKey.vue.b2c184d1.js → PhKey.vue.15a9e64e.js} +2 -2
- abstra_statics/dist/assets/{PhPencil.vue.2f2fe576.js → PhPencil.vue.a7219766.js} +2 -2
- abstra_statics/dist/assets/PhPencilSimple.vue.15a2b403.js +2 -0
- abstra_statics/dist/assets/PhRocket.vue.7155b91f.js +2 -0
- abstra_statics/dist/assets/{PhSignOut.vue.a271f14b.js → PhSignOut.vue.2af17bd7.js} +2 -2
- abstra_statics/dist/assets/{PhSparkle.vue.726defa2.js → PhSparkle.vue.c7f06cac.js} +2 -2
- abstra_statics/dist/assets/PhTranslate.vue.2ce651a6.js +2 -0
- abstra_statics/dist/assets/{PhUsersThree.vue.275d13ab.js → PhUsersThree.vue.2942df75.js} +2 -2
- abstra_statics/dist/assets/{PhWarningCircle.vue.3b1aca1b.js → PhWarningCircle.vue.05a40bc4.js} +2 -2
- abstra_statics/dist/assets/PhWebhooksLogo.vue.e4752384.js +2 -0
- abstra_statics/dist/assets/PlayerConfigProvider.00af5968.js +2 -0
- abstra_statics/dist/assets/PlayerNavbar.117f184b.js +2 -0
- abstra_statics/dist/assets/{PlayerNavbar.77209eae.css → PlayerNavbar.4dc29a45.css} +1 -1
- abstra_statics/dist/assets/Project.66111161.js +2 -0
- abstra_statics/dist/assets/Project.9c418d2e.css +1 -0
- abstra_statics/dist/assets/ProjectLogin.d9bb1f86.js +2 -0
- abstra_statics/dist/assets/ProjectSettings.f8c6f60a.js +2 -0
- abstra_statics/dist/assets/ProjectsView.32f6ccff.js +2 -0
- abstra_statics/dist/assets/ProjectsView.e26ddfd5.css +1 -0
- abstra_statics/dist/assets/SaveButton.363ea20f.css +1 -0
- abstra_statics/dist/assets/SaveButton.c3f2a4bb.js +2 -0
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.cb5567cd.js +2 -0
- abstra_statics/dist/assets/{Sidebar.29baeab0.css → Sidebar.161522da.css} +1 -1
- abstra_statics/dist/assets/Sidebar.1c4e35be.js +2 -0
- abstra_statics/dist/assets/Sql.6d9a778c.css +1 -0
- abstra_statics/dist/assets/Sql.8d31ec23.js +5 -0
- abstra_statics/dist/assets/Steps.687763a5.js +2 -0
- abstra_statics/dist/assets/TableCard.8c99a870.js +2 -0
- abstra_statics/dist/assets/{TableEditor.5853a363.css → TableEditor.20fecc75.css} +1 -1
- abstra_statics/dist/assets/TableEditor.ba7a8b6a.js +2 -0
- abstra_statics/dist/assets/Tables.113960f2.js +39 -0
- abstra_statics/dist/assets/TablesDiagram.8e6d1e89.js +15 -0
- abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.5cc96b0d.js +2 -0
- abstra_statics/dist/assets/Tasks.90846020.js +2 -0
- abstra_statics/dist/assets/{UploadOutlined.eab75eb0.js → UploadOutlined.518baf9a.js} +2 -2
- abstra_statics/dist/assets/View.ded6b355.js +2 -0
- abstra_statics/dist/assets/View.vue_vue_type_script_setup_true_lang.285b5e2c.js +2 -0
- abstra_statics/dist/assets/Watermark.5071a4b2.js +2 -0
- abstra_statics/dist/assets/WebEditor.18ece735.js +2 -0
- abstra_statics/dist/assets/WebEditor.b886e4d1.css +1 -0
- abstra_statics/dist/assets/WidgetPreview.88a4f27f.js +2 -0
- abstra_statics/dist/assets/WorkflowViewer.3b6aee8e.css +1 -0
- abstra_statics/dist/assets/WorkflowViewer.778c401d.js +2 -0
- abstra_statics/dist/assets/ant-design.4efc9ccd.js +2 -0
- abstra_statics/dist/assets/{apiKey.72f497ca.js → apiKey.bd946d8c.js} +2 -2
- abstra_statics/dist/assets/asyncComputed.7bc1692e.js +2 -0
- abstra_statics/dist/assets/{build.df2d55cc.js → build.a8637e29.js} +2 -2
- abstra_statics/dist/assets/colorHelpers.8ba18214.js +2 -0
- abstra_statics/dist/assets/{console.2bf7f04d.js → console.2a5ed51a.js} +4 -4
- abstra_statics/dist/assets/constants.534f67bc.js +2 -0
- abstra_statics/dist/assets/contracts.generated.8ad36e63.js +2 -0
- abstra_statics/dist/assets/{cssMode.7133c7cb.js → cssMode.6c4ccf50.js} +2 -2
- abstra_statics/dist/assets/{datetime.8de2ff28.js → datetime.89495471.js} +2 -2
- abstra_statics/dist/assets/dayjs.304f38f8.js +2 -0
- abstra_statics/dist/assets/editor.c1a1bd33.js +2 -0
- abstra_statics/dist/assets/editor.main.84e237cf.js +2 -0
- abstra_statics/dist/assets/fetch.452c58e5.js +2 -0
- abstra_statics/dist/assets/files.8999afd5.js +2 -0
- abstra_statics/dist/assets/{folder.d8e23009.js → folder.81ef8619.js} +2 -2
- abstra_statics/dist/assets/{freemarker2.6698d1ea.js → freemarker2.559f77f2.js} +2 -2
- abstra_statics/dist/assets/{handlebars.a6c42dc0.js → handlebars.8d101b7c.js} +2 -2
- abstra_statics/dist/assets/{html.493a5410.js → html.b3e7d3ab.js} +3 -3
- abstra_statics/dist/assets/{htmlMode.a28b2fca.js → htmlMode.2305b1bb.js} +2 -2
- abstra_statics/dist/assets/{index.fb49354b.js → index.71eb83f3.js} +2 -2
- abstra_statics/dist/assets/{index.79ce3bf1.js → index.90acf038.js} +2 -2
- abstra_statics/dist/assets/index.b74c262c.js +4 -0
- abstra_statics/dist/assets/index.bc97991a.js +2 -0
- abstra_statics/dist/assets/index.d809956c.js +2 -0
- abstra_statics/dist/assets/{index.57042181.css → index.da037bc0.css} +1 -1
- abstra_statics/dist/assets/index.da4f9d54.js +2 -0
- abstra_statics/dist/assets/{index.c34a405a.js → index.e5cb42a1.js} +2 -2
- abstra_statics/dist/assets/{index.5d6b1e62.js → index.f2beb20d.js} +2 -2
- abstra_statics/dist/assets/{index.4d20c159.js → index.f6171691.js} +3 -3
- abstra_statics/dist/assets/{javascript.3a36cf17.js → javascript.3000fc25.js} +3 -3
- abstra_statics/dist/assets/{jsonMode.bead6ac8.js → jsonMode.7bbb508d.js} +2 -2
- abstra_statics/dist/assets/{jwt-decode.9f7a5511.css → jwt-decode.c5760184.css} +2 -2
- abstra_statics/dist/assets/{jwt-decode.esm.d4517a10.js → jwt-decode.esm.c9c37cdc.js} +197 -197
- abstra_statics/dist/assets/{linters.2f3141cb.js → linters.7fec18d9.js} +2 -2
- abstra_statics/dist/assets/{liquid.0c337fae.js → liquid.b4ac9aaf.js} +3 -3
- abstra_statics/dist/assets/member.3c12efee.js +2 -0
- abstra_statics/dist/assets/metadata.e627ddda.js +2 -0
- abstra_statics/dist/assets/omniChatStore.508e8ece.js +9 -0
- abstra_statics/dist/assets/omniChatStore.b58e3bed.css +1 -0
- abstra_statics/dist/assets/{organization.928c9bef.js → organization.cd03f9a8.js} +2 -2
- abstra_statics/dist/assets/os.f08724fb.js +2 -0
- abstra_statics/dist/assets/player.7362caf4.js +2 -0
- abstra_statics/dist/assets/{plotly.min.7225d3a0.js → plotly.min.50ebb925.js} +2 -2
- abstra_statics/dist/assets/polling.4db5ee9a.js +2 -0
- abstra_statics/dist/assets/{project.619b7244.js → project.9a068e8d.js} +2 -2
- abstra_statics/dist/assets/{python.05764499.js → python.51a7c648.js} +3 -3
- abstra_statics/dist/assets/{razor.81a45581.js → razor.99323f5f.js} +3 -3
- abstra_statics/dist/assets/record.a33d29b1.js +2 -0
- abstra_statics/dist/assets/{redirect.f028a879.js → redirect.42bf4f0a.js} +2 -2
- abstra_statics/dist/assets/repository.5190b94f.js +2 -0
- abstra_statics/dist/assets/repository.94fb77c7.js +2 -0
- abstra_statics/dist/assets/repository.c0d70cb2.js +2 -0
- abstra_statics/dist/assets/router.4168cc71.js +2 -0
- abstra_statics/dist/assets/{router.424f7da9.js → router.a8616541.js} +5 -5
- abstra_statics/dist/assets/string.8fab6b53.js +2 -0
- abstra_statics/dist/assets/{tables.1f68ec62.js → tables.2e1c934b.js} +2 -2
- abstra_statics/dist/assets/tasksController.1feffcfe.js +4 -0
- abstra_statics/dist/assets/{toggleHighContrast.0d0e5662.js → toggleHighContrast.6544a728.js} +7 -7
- abstra_statics/dist/assets/{tsMode.6eadbf06.js → tsMode.922e04bb.js} +2 -2
- abstra_statics/dist/assets/{typescript.1670e287.js → typescript.1b4f8286.js} +3 -3
- abstra_statics/dist/assets/url.9e033350.js +2 -0
- abstra_statics/dist/assets/{useCodebaseEvents.53dec1f2.js → useCodebaseEvents.ffe057d1.js} +2 -2
- abstra_statics/dist/assets/useTables.5fffa3f1.js +2 -0
- abstra_statics/dist/assets/userStore.d962fba4.js +2 -0
- abstra_statics/dist/assets/uuid.8581bc03.js +2 -0
- abstra_statics/dist/assets/vue-flow-background.3e9183ec.js +2 -0
- abstra_statics/dist/assets/vue-flow-core.41c647da.js +22 -0
- abstra_statics/dist/assets/{vue-quill.esm-bundler.c4f04985.js → vue-quill.esm-bundler.36e79a95.js} +2 -2
- abstra_statics/dist/assets/{workspaceStore.6244d03d.js → workspaceStore.5a435520.js} +2 -2
- abstra_statics/dist/assets/{xml.f2867af8.js → xml.c1692f52.js} +3 -3
- abstra_statics/dist/assets/{yaml.5427bb1b.js → yaml.244444c1.js} +2 -2
- abstra_statics/dist/console.html +15 -15
- abstra_statics/dist/editor.html +15 -15
- abstra_statics/dist/player.html +10 -10
- abstra/connectors/banking/__init__.py +0 -0
- abstra/connectors/banking/brazil.py +0 -5
- abstra_internals/repositories/git/dulwich.py +0 -1353
- abstra_internals/services/banking/__init__.py +0 -3
- abstra_internals/services/banking/banking_service.py +0 -104
- abstra_internals/services/banking/client_factory.py +0 -70
- abstra_internals/services/banking/sdk/__init__.py +0 -0
- abstra_internals/services/banking/sdk/generate_totalbank_api.py +0 -383
- abstra_statics/dist/assets/AbstraButton.vue_vue_type_script_setup_true_lang.eb8ccb64.js +0 -2
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.9f98292e.js +0 -2
- abstra_statics/dist/assets/ApiKeys.e975c4f3.js +0 -2
- abstra_statics/dist/assets/App.f62faff6.js +0 -2
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.5aa45ac1.js +0 -2
- abstra_statics/dist/assets/BaseLayout.1ec2c96d.js +0 -2
- abstra_statics/dist/assets/Billing.60adc9fa.js +0 -2
- abstra_statics/dist/assets/Builds.ace9e3da.css +0 -1
- abstra_statics/dist/assets/Builds.f86210bc.js +0 -2
- abstra_statics/dist/assets/CloseCircleOutlined.30bc25a8.js +0 -2
- abstra_statics/dist/assets/ConnectorsView.33c5380f.css +0 -1
- abstra_statics/dist/assets/ConnectorsView.eb4c769f.js +0 -2
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.8d2e4672.js +0 -2
- abstra_statics/dist/assets/ContentLayout.d03fee5b.js +0 -2
- abstra_statics/dist/assets/CrudView.c0824225.js +0 -2
- abstra_statics/dist/assets/CrudView.e24590ae.css +0 -1
- abstra_statics/dist/assets/DocsButton.vue_vue_type_script_setup_true_lang.3668aee4.js +0 -2
- abstra_statics/dist/assets/EditorLogin.46db248f.js +0 -2
- abstra_statics/dist/assets/EditorsView.e72621fa.js +0 -2
- abstra_statics/dist/assets/EnvVars.f9f9d61f.js +0 -2
- abstra_statics/dist/assets/Error.864f05b3.js +0 -2
- abstra_statics/dist/assets/ExclamationCircleOutlined.a489b996.js +0 -2
- abstra_statics/dist/assets/Files.afe615e1.js +0 -2
- abstra_statics/dist/assets/Files.d0e8d2ff.css +0 -1
- abstra_statics/dist/assets/Form.556d0de2.js +0 -2
- abstra_statics/dist/assets/FormRunner.7f56a8c6.js +0 -2
- abstra_statics/dist/assets/Home.287d17f8.js +0 -2
- abstra_statics/dist/assets/Home.3794e8b4.css +0 -1
- abstra_statics/dist/assets/Home.5b7e9c23.js +0 -2
- abstra_statics/dist/assets/Live.37415b2d.css +0 -1
- abstra_statics/dist/assets/Live.50bacfea.js +0 -2
- abstra_statics/dist/assets/LoadingContainer.ebace8de.js +0 -2
- abstra_statics/dist/assets/LoadingOutlined.9e949112.js +0 -2
- abstra_statics/dist/assets/Login.536a3067.js +0 -2
- abstra_statics/dist/assets/Login.bec408c9.js +0 -2
- abstra_statics/dist/assets/Login.vue_vue_type_script_setup_true_lang.1c3f108d.js +0 -2
- abstra_statics/dist/assets/Logo.82d6ab70.js +0 -2
- abstra_statics/dist/assets/Logs.f6135084.js +0 -2
- abstra_statics/dist/assets/LogsController.6b666816.js +0 -2
- abstra_statics/dist/assets/LogsController.fb0d96c2.css +0 -1
- abstra_statics/dist/assets/Main.77b115f8.js +0 -2
- abstra_statics/dist/assets/Navbar.2dc6d02c.css +0 -1
- abstra_statics/dist/assets/Navbar.4a6f2b09.js +0 -2
- abstra_statics/dist/assets/NewEditor.5f84de86.css +0 -1
- abstra_statics/dist/assets/NewEditor.e558e47d.js +0 -8
- abstra_statics/dist/assets/OidcLoginCallback.7f514b45.js +0 -2
- abstra_statics/dist/assets/OidcLogoutCallback.038813a1.js +0 -2
- abstra_statics/dist/assets/OmniChat.05ba8d8a.css +0 -1
- abstra_statics/dist/assets/OmniChat.60d98deb.js +0 -6
- abstra_statics/dist/assets/OnboardingView.3996b08d.css +0 -1
- abstra_statics/dist/assets/OnboardingView.9413ee50.js +0 -2
- abstra_statics/dist/assets/Organization.7203cc0b.js +0 -2
- abstra_statics/dist/assets/Organizations.91220ca0.js +0 -2
- abstra_statics/dist/assets/PhBookBookmark.vue.5b7ab079.js +0 -2
- abstra_statics/dist/assets/PhChats.vue.b5df7174.js +0 -2
- abstra_statics/dist/assets/PhCopySimple.vue.d41d9160.js +0 -2
- abstra_statics/dist/assets/PhDatabase.vue.edfcb96b.js +0 -2
- abstra_statics/dist/assets/PhPencilSimple.vue.cc8620ae.js +0 -2
- abstra_statics/dist/assets/PhRocket.vue.e397203c.js +0 -2
- abstra_statics/dist/assets/PhUserList.vue.6a29f16d.js +0 -2
- abstra_statics/dist/assets/PhWebhooksLogo.vue.5e772aac.js +0 -2
- abstra_statics/dist/assets/PlayerConfigProvider.e90a2b41.js +0 -2
- abstra_statics/dist/assets/PlayerNavbar.11ec1844.js +0 -2
- abstra_statics/dist/assets/Project.9c75c141.css +0 -1
- abstra_statics/dist/assets/Project.c16740fb.js +0 -2
- abstra_statics/dist/assets/ProjectLogin.e7a6f444.js +0 -2
- abstra_statics/dist/assets/ProjectSettings.52c19693.js +0 -2
- abstra_statics/dist/assets/ProjectsView.16d8ecf6.css +0 -1
- abstra_statics/dist/assets/ProjectsView.22fa7a8e.js +0 -2
- abstra_statics/dist/assets/SaveButton.719393d2.js +0 -2
- abstra_statics/dist/assets/SaveButton.932ac6b8.css +0 -1
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.d4028954.js +0 -2
- abstra_statics/dist/assets/Sidebar.5cb8e04e.js +0 -2
- abstra_statics/dist/assets/Sql.23d80bad.js +0 -5
- abstra_statics/dist/assets/Sql.90e6e2ba.css +0 -1
- abstra_statics/dist/assets/Steps.8e5d201a.js +0 -2
- abstra_statics/dist/assets/TableCard.59f95f8f.js +0 -2
- abstra_statics/dist/assets/TableEditor.8539f984.js +0 -2
- abstra_statics/dist/assets/Tables.44d953f7.js +0 -2
- abstra_statics/dist/assets/TablesDiagram.8e47383c.js +0 -15
- abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.6866fb32.js +0 -2
- abstra_statics/dist/assets/Tasks.09551b19.js +0 -2
- abstra_statics/dist/assets/View.5fd7ddf0.js +0 -2
- abstra_statics/dist/assets/View.vue_vue_type_script_setup_true_lang.a904f400.js +0 -2
- abstra_statics/dist/assets/Watermark.ab3d818f.js +0 -2
- abstra_statics/dist/assets/WebEditor.c2e271d1.css +0 -1
- abstra_statics/dist/assets/WebEditor.d6ec6392.js +0 -2
- abstra_statics/dist/assets/WidgetPreview.86b31dec.js +0 -2
- abstra_statics/dist/assets/ant-design.a865486e.js +0 -2
- abstra_statics/dist/assets/asyncComputed.cf5282fc.js +0 -2
- abstra_statics/dist/assets/colorHelpers.37d9932b.js +0 -2
- abstra_statics/dist/assets/constants.7d38ec8b.js +0 -2
- abstra_statics/dist/assets/contracts.generated.590b1102.js +0 -2
- abstra_statics/dist/assets/dayjs.f18bbbca.js +0 -2
- abstra_statics/dist/assets/editor.3a4714e3.js +0 -2
- abstra_statics/dist/assets/editor.main.9c635b9a.js +0 -2
- abstra_statics/dist/assets/fetch.89fd5b7b.js +0 -2
- abstra_statics/dist/assets/index.2af3391c.js +0 -4
- abstra_statics/dist/assets/index.4176fe88.js +0 -2
- abstra_statics/dist/assets/index.fb182bd1.js +0 -2
- abstra_statics/dist/assets/member.48d6f2cd.js +0 -2
- abstra_statics/dist/assets/metadata.c3aed6e1.js +0 -2
- abstra_statics/dist/assets/omniChatStore.c53bcca2.js +0 -8
- abstra_statics/dist/assets/omniChatStore.ec95fb81.css +0 -1
- abstra_statics/dist/assets/player.d3aeafc5.js +0 -2
- abstra_statics/dist/assets/polling.82ee6b45.js +0 -2
- abstra_statics/dist/assets/record.7f43486c.js +0 -2
- abstra_statics/dist/assets/repository.9534db4b.js +0 -2
- abstra_statics/dist/assets/repository.c15239ce.js +0 -2
- abstra_statics/dist/assets/router.262190ec.js +0 -2
- abstra_statics/dist/assets/string.0acf5572.js +0 -2
- abstra_statics/dist/assets/tasksController.371896de.js +0 -4
- abstra_statics/dist/assets/url.e8732f77.js +0 -2
- abstra_statics/dist/assets/useTables.4f034cf8.js +0 -2
- abstra_statics/dist/assets/userStore.31024da3.js +0 -2
- abstra_statics/dist/assets/uuid.bde15ce7.js +0 -2
- abstra_statics/dist/assets/vue-flow-background.818c7852.js +0 -2
- abstra_statics/dist/assets/vue-flow-core.1180ec83.js +0 -22
- {abstra-3.24.0.dist-info → abstra-3.24.2.dist-info}/WHEEL +0 -0
- {abstra-3.24.0.dist-info → abstra-3.24.2.dist-info}/entry_points.txt +0 -0
- {abstra-3.24.0.dist-info → abstra-3.24.2.dist-info}/top_level.txt +0 -0
|
@@ -1,1353 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING, List, Optional, Tuple
|
|
5
|
-
from urllib.parse import urlparse
|
|
6
|
-
|
|
7
|
-
import urllib3
|
|
8
|
-
|
|
9
|
-
from abstra_internals.environment import REMOTE_NAME
|
|
10
|
-
from abstra_internals.logger import AbstraLogger
|
|
11
|
-
|
|
12
|
-
from .types import GitCommit, GitFileChange, GitRepositoryInterface, GitStatus
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
from dulwich import porcelain
|
|
16
|
-
from dulwich.client import HttpGitClient
|
|
17
|
-
from dulwich.errors import NotGitRepository
|
|
18
|
-
from dulwich.index import build_index_from_tree
|
|
19
|
-
from dulwich.objects import Commit
|
|
20
|
-
from dulwich.porcelain import reset
|
|
21
|
-
from dulwich.repo import Repo
|
|
22
|
-
from dulwich.stash import Stash
|
|
23
|
-
|
|
24
|
-
DULWICH_AVAILABLE = True
|
|
25
|
-
except ImportError:
|
|
26
|
-
DULWICH_AVAILABLE = False
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from dulwich import porcelain
|
|
29
|
-
from dulwich.client import HttpGitClient
|
|
30
|
-
from dulwich.errors import NotGitRepository
|
|
31
|
-
from dulwich.index import build_index_from_tree
|
|
32
|
-
from dulwich.objects import Commit
|
|
33
|
-
from dulwich.porcelain import reset
|
|
34
|
-
from dulwich.repo import Repo
|
|
35
|
-
from dulwich.stash import Stash
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class DulwichGitRepository(GitRepositoryInterface):
|
|
39
|
-
"""Repository for Git operations using Dulwich library"""
|
|
40
|
-
|
|
41
|
-
def __init__(self, working_directory: Path):
|
|
42
|
-
super().__init__(working_directory)
|
|
43
|
-
|
|
44
|
-
self._repo: Optional["Repo"] = None # type: ignore
|
|
45
|
-
self._try_load_repo()
|
|
46
|
-
|
|
47
|
-
def _try_load_repo(self):
|
|
48
|
-
"""Try to load existing repository"""
|
|
49
|
-
try:
|
|
50
|
-
if self.is_git_repository():
|
|
51
|
-
self._repo = Repo(str(self.working_directory)) # type: ignore
|
|
52
|
-
except Exception:
|
|
53
|
-
self._repo = None
|
|
54
|
-
|
|
55
|
-
def configure_git_user(self, fallback_email: str, fallback_name: str):
|
|
56
|
-
"""Ensure git user is configured for commits (required in CI environments)"""
|
|
57
|
-
if not self._repo:
|
|
58
|
-
return
|
|
59
|
-
try:
|
|
60
|
-
config = self._repo.get_config()
|
|
61
|
-
try:
|
|
62
|
-
config.get((b"user",), b"name")
|
|
63
|
-
except KeyError:
|
|
64
|
-
config.set((b"user",), b"name", fallback_name.encode())
|
|
65
|
-
try:
|
|
66
|
-
config.get((b"user",), b"email")
|
|
67
|
-
except KeyError:
|
|
68
|
-
config.set((b"user",), b"email", fallback_email.encode())
|
|
69
|
-
config.write_to_path()
|
|
70
|
-
except Exception:
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
def is_git_available(self) -> bool:
|
|
74
|
-
"""Check if dulwich is available"""
|
|
75
|
-
return DULWICH_AVAILABLE
|
|
76
|
-
|
|
77
|
-
def find_git_root(self, start_path: Optional[Path] = None) -> Optional[Path]:
|
|
78
|
-
"""Find the root directory of the git repository"""
|
|
79
|
-
if start_path is None:
|
|
80
|
-
start_path = self.working_directory
|
|
81
|
-
|
|
82
|
-
if start_path is None:
|
|
83
|
-
start_path = Path.cwd()
|
|
84
|
-
|
|
85
|
-
current = start_path.resolve()
|
|
86
|
-
while current != current.parent:
|
|
87
|
-
if (current / ".git").exists():
|
|
88
|
-
return current
|
|
89
|
-
current = current.parent
|
|
90
|
-
return None
|
|
91
|
-
|
|
92
|
-
def is_git_repository(self) -> bool:
|
|
93
|
-
"""Check if current directory is a git repository"""
|
|
94
|
-
if not self.is_git_available():
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
pass # Ensure try block has at least one except clause
|
|
99
|
-
Repo(str(self.working_directory)) # type: ignore
|
|
100
|
-
return True
|
|
101
|
-
except (NotGitRepository, Exception): # type: ignore
|
|
102
|
-
return False
|
|
103
|
-
|
|
104
|
-
def init_repository(self) -> bool:
|
|
105
|
-
"""Initialize a new git repository in the working directory"""
|
|
106
|
-
if not self.is_git_available():
|
|
107
|
-
return False
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
self._repo = porcelain.init(str(self.working_directory))
|
|
111
|
-
|
|
112
|
-
# Set HEAD to point to main branch instead of master BEFORE any commits
|
|
113
|
-
# This ensures the first commit goes to main branch
|
|
114
|
-
self._repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/main")
|
|
115
|
-
|
|
116
|
-
add_success = True
|
|
117
|
-
try:
|
|
118
|
-
porcelain.add(
|
|
119
|
-
str(self.working_directory), [str(self.working_directory)]
|
|
120
|
-
)
|
|
121
|
-
except Exception:
|
|
122
|
-
add_success = False
|
|
123
|
-
|
|
124
|
-
commit_success = False
|
|
125
|
-
|
|
126
|
-
if add_success:
|
|
127
|
-
try:
|
|
128
|
-
status = porcelain.status(str(self.working_directory))
|
|
129
|
-
has_staged_files = (
|
|
130
|
-
len(status.staged["add"]) > 0
|
|
131
|
-
or len(status.staged["modify"]) > 0
|
|
132
|
-
or len(status.staged["delete"]) > 0
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if has_staged_files:
|
|
136
|
-
# Commit staged files
|
|
137
|
-
porcelain.commit(str(self.working_directory), b"First commit")
|
|
138
|
-
commit_success = True
|
|
139
|
-
except Exception:
|
|
140
|
-
pass
|
|
141
|
-
|
|
142
|
-
# If we haven't created a commit yet, create an empty one
|
|
143
|
-
if not commit_success:
|
|
144
|
-
try:
|
|
145
|
-
# Create empty commit using porcelain - this should work even with no files
|
|
146
|
-
# We need to ensure the index exists first
|
|
147
|
-
index_path = os.path.join(
|
|
148
|
-
str(self.working_directory), ".git", "index"
|
|
149
|
-
)
|
|
150
|
-
if not os.path.exists(index_path):
|
|
151
|
-
# Touch the index file to create it
|
|
152
|
-
Path(index_path).touch()
|
|
153
|
-
|
|
154
|
-
porcelain.commit(str(self.working_directory), b"First commit")
|
|
155
|
-
commit_success = True
|
|
156
|
-
except Exception:
|
|
157
|
-
# Last resort: create commit manually
|
|
158
|
-
try:
|
|
159
|
-
import time
|
|
160
|
-
|
|
161
|
-
from dulwich.objects import Commit as DulwichCommit
|
|
162
|
-
from dulwich.objects import Tree
|
|
163
|
-
|
|
164
|
-
# Create empty tree
|
|
165
|
-
empty_tree = Tree()
|
|
166
|
-
tree_id = self._repo.object_store.add_object(empty_tree)
|
|
167
|
-
|
|
168
|
-
# Create commit object
|
|
169
|
-
commit = DulwichCommit()
|
|
170
|
-
commit.tree = tree_id
|
|
171
|
-
commit.author = commit.committer = (
|
|
172
|
-
b"Abstra <no-reply@abstra.app>"
|
|
173
|
-
)
|
|
174
|
-
commit.commit_time = commit.author_time = int(time.time())
|
|
175
|
-
commit.commit_timezone = commit.author_timezone = 0
|
|
176
|
-
commit.message = b"First commit"
|
|
177
|
-
|
|
178
|
-
commit_id = self._repo.object_store.add_object(commit)
|
|
179
|
-
# commit_id may be None per dulwich typing; only set if present
|
|
180
|
-
if commit_id is not None:
|
|
181
|
-
self._repo.refs[b"refs/heads/main"] = commit_id
|
|
182
|
-
commit_success = True
|
|
183
|
-
except Exception:
|
|
184
|
-
pass
|
|
185
|
-
|
|
186
|
-
if commit_success:
|
|
187
|
-
# After commit, remove the master branch reference if it was created
|
|
188
|
-
try:
|
|
189
|
-
if b"refs/heads/master" in self._repo.refs:
|
|
190
|
-
del self._repo.refs[b"refs/heads/master"]
|
|
191
|
-
except Exception:
|
|
192
|
-
pass
|
|
193
|
-
|
|
194
|
-
return True
|
|
195
|
-
except Exception:
|
|
196
|
-
return False
|
|
197
|
-
|
|
198
|
-
def get_current_branch(self) -> Optional[str]:
|
|
199
|
-
"""Get the current branch name"""
|
|
200
|
-
if not self._repo:
|
|
201
|
-
return None
|
|
202
|
-
|
|
203
|
-
try:
|
|
204
|
-
# Get HEAD reference from dulwich refs (more reliable than file system)
|
|
205
|
-
head_ref = self._repo.refs[b"HEAD"] # type: ignore
|
|
206
|
-
if head_ref.startswith(b"ref: "):
|
|
207
|
-
ref_path = head_ref[5:].decode("utf-8")
|
|
208
|
-
if ref_path.startswith("refs/heads/"):
|
|
209
|
-
return ref_path[11:] # Remove "refs/heads/"
|
|
210
|
-
|
|
211
|
-
# For repositories without commits, fallback to reading HEAD file directly
|
|
212
|
-
try:
|
|
213
|
-
head_file = os.path.join(self._repo.path, ".git", "HEAD")
|
|
214
|
-
with open(head_file, "r", encoding="utf-8") as f:
|
|
215
|
-
head_content = f.read().strip()
|
|
216
|
-
if head_content.startswith("ref: refs/heads/"):
|
|
217
|
-
return head_content[16:] # Remove "ref: refs/heads/"
|
|
218
|
-
except Exception:
|
|
219
|
-
pass
|
|
220
|
-
|
|
221
|
-
return None
|
|
222
|
-
except Exception:
|
|
223
|
-
return None
|
|
224
|
-
|
|
225
|
-
def get_all_branches(self) -> List[str]:
|
|
226
|
-
"""Get all local branches"""
|
|
227
|
-
if not self._repo:
|
|
228
|
-
return []
|
|
229
|
-
|
|
230
|
-
if not self._repo:
|
|
231
|
-
return []
|
|
232
|
-
try:
|
|
233
|
-
branches = []
|
|
234
|
-
for ref_name, _ in self._repo.refs.as_dict().items(): # type: ignore
|
|
235
|
-
if ref_name.startswith(b"refs/heads/"):
|
|
236
|
-
branch_name = ref_name[11:].decode("utf-8") # Remove "refs/heads/"
|
|
237
|
-
branches.append(branch_name)
|
|
238
|
-
return branches
|
|
239
|
-
except Exception:
|
|
240
|
-
return []
|
|
241
|
-
|
|
242
|
-
def get_last_commit(self) -> Optional[GitCommit]:
|
|
243
|
-
"""Get information about the last commit"""
|
|
244
|
-
commits = self.get_commit_history(limit=1)
|
|
245
|
-
return commits[0] if commits else None
|
|
246
|
-
|
|
247
|
-
def get_commit_history(
|
|
248
|
-
self, limit: int = 10, offset: int = 0, branch: Optional[str] = None
|
|
249
|
-
) -> List[GitCommit]:
|
|
250
|
-
"""Get commit history with pagination"""
|
|
251
|
-
if not self._repo:
|
|
252
|
-
return []
|
|
253
|
-
|
|
254
|
-
try:
|
|
255
|
-
commits = []
|
|
256
|
-
|
|
257
|
-
if branch:
|
|
258
|
-
# Get commits from specific branch only
|
|
259
|
-
try:
|
|
260
|
-
branch_ref = f"refs/heads/{branch}".encode()
|
|
261
|
-
if branch_ref in self._repo.refs:
|
|
262
|
-
ref_sha = self._repo.refs[branch_ref]
|
|
263
|
-
all_refs = [ref_sha]
|
|
264
|
-
else:
|
|
265
|
-
# Try remote branch
|
|
266
|
-
remote_ref = f"refs/remotes/origin/{branch}".encode()
|
|
267
|
-
if remote_ref in self._repo.refs:
|
|
268
|
-
ref_sha = self._repo.refs[remote_ref]
|
|
269
|
-
all_refs = [ref_sha]
|
|
270
|
-
else:
|
|
271
|
-
return []
|
|
272
|
-
except Exception:
|
|
273
|
-
return []
|
|
274
|
-
else:
|
|
275
|
-
# Get all branch refs to include commits from all branches
|
|
276
|
-
all_refs = []
|
|
277
|
-
for ref_name, ref_sha in self._repo.refs.as_dict().items():
|
|
278
|
-
if ref_name.startswith(b"refs/heads/") or ref_name.startswith(
|
|
279
|
-
b"refs/remotes/"
|
|
280
|
-
):
|
|
281
|
-
all_refs.append(ref_sha)
|
|
282
|
-
|
|
283
|
-
# If no refs found, fallback to HEAD
|
|
284
|
-
if not all_refs:
|
|
285
|
-
all_refs = [self._repo.head()]
|
|
286
|
-
|
|
287
|
-
walker = self._repo.get_walker(include=all_refs, max_entries=limit + offset) # type: ignore
|
|
288
|
-
|
|
289
|
-
# Skip offset entries
|
|
290
|
-
for i, entry in enumerate(walker):
|
|
291
|
-
if i < offset:
|
|
292
|
-
continue
|
|
293
|
-
if len(commits) >= limit:
|
|
294
|
-
break
|
|
295
|
-
|
|
296
|
-
commit = entry.commit
|
|
297
|
-
# Convert timestamp to date string to match native format
|
|
298
|
-
date_obj = datetime.datetime.fromtimestamp(commit.commit_time)
|
|
299
|
-
# Use ISO format with time, matching native implementation
|
|
300
|
-
date_str = date_obj.strftime("%Y-%m-%d %H:%M:%S")
|
|
301
|
-
|
|
302
|
-
commits.append(
|
|
303
|
-
GitCommit(
|
|
304
|
-
hash=commit.id.decode("ascii"),
|
|
305
|
-
message=commit.message.decode("utf-8", errors="ignore").strip(),
|
|
306
|
-
author=commit.author.decode("utf-8", errors="ignore")
|
|
307
|
-
.split("<")[0]
|
|
308
|
-
.strip(),
|
|
309
|
-
date=date_str,
|
|
310
|
-
)
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
return commits
|
|
314
|
-
except Exception:
|
|
315
|
-
return []
|
|
316
|
-
|
|
317
|
-
def get_changed_files(self) -> List[str]:
|
|
318
|
-
"""Get list of changed files"""
|
|
319
|
-
if not self._repo:
|
|
320
|
-
return []
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
# Get status using dulwich - porcelain.status expects repo path
|
|
324
|
-
status = porcelain.status(str(self.working_directory)) # type: ignore
|
|
325
|
-
|
|
326
|
-
changed_files = []
|
|
327
|
-
|
|
328
|
-
# Add staged files
|
|
329
|
-
for file_path in status.staged["add"]: # type: ignore
|
|
330
|
-
if isinstance(file_path, bytes):
|
|
331
|
-
changed_files.append(file_path.decode("utf-8"))
|
|
332
|
-
else:
|
|
333
|
-
changed_files.append(file_path)
|
|
334
|
-
for file_path in status.staged["delete"]: # type: ignore
|
|
335
|
-
if isinstance(file_path, bytes):
|
|
336
|
-
changed_files.append(file_path.decode("utf-8"))
|
|
337
|
-
else:
|
|
338
|
-
changed_files.append(file_path)
|
|
339
|
-
for file_path in status.staged["modify"]: # type: ignore
|
|
340
|
-
if isinstance(file_path, bytes):
|
|
341
|
-
changed_files.append(file_path.decode("utf-8"))
|
|
342
|
-
else:
|
|
343
|
-
changed_files.append(file_path)
|
|
344
|
-
|
|
345
|
-
# Add unstaged files
|
|
346
|
-
for file_path in status.unstaged: # type: ignore
|
|
347
|
-
if isinstance(file_path, bytes):
|
|
348
|
-
changed_files.append(file_path.decode("utf-8"))
|
|
349
|
-
else:
|
|
350
|
-
changed_files.append(file_path)
|
|
351
|
-
|
|
352
|
-
# Add untracked files
|
|
353
|
-
for file_path in status.untracked: # type: ignore
|
|
354
|
-
if isinstance(file_path, bytes):
|
|
355
|
-
changed_files.append(file_path.decode("utf-8"))
|
|
356
|
-
else:
|
|
357
|
-
changed_files.append(file_path)
|
|
358
|
-
|
|
359
|
-
return sorted(list(set(changed_files)))
|
|
360
|
-
except Exception:
|
|
361
|
-
return []
|
|
362
|
-
|
|
363
|
-
def get_changed_files_with_status(self) -> List[GitFileChange]:
|
|
364
|
-
"""Get list of changed files with their git status"""
|
|
365
|
-
if not self._repo:
|
|
366
|
-
return []
|
|
367
|
-
|
|
368
|
-
try:
|
|
369
|
-
# Get status using dulwich - porcelain.status expects repo path
|
|
370
|
-
status = porcelain.status(str(self.working_directory)) # type: ignore
|
|
371
|
-
|
|
372
|
-
files = []
|
|
373
|
-
|
|
374
|
-
# Add staged files
|
|
375
|
-
for file_path in status.staged["add"]: # type: ignore
|
|
376
|
-
filename = (
|
|
377
|
-
file_path.decode("utf-8")
|
|
378
|
-
if isinstance(file_path, bytes)
|
|
379
|
-
else str(file_path)
|
|
380
|
-
)
|
|
381
|
-
files.append(
|
|
382
|
-
GitFileChange(path=filename, status="added", status_code="A ")
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
for file_path in status.staged["delete"]: # type: ignore
|
|
386
|
-
filename = (
|
|
387
|
-
file_path.decode("utf-8")
|
|
388
|
-
if isinstance(file_path, bytes)
|
|
389
|
-
else str(file_path)
|
|
390
|
-
)
|
|
391
|
-
files.append(
|
|
392
|
-
GitFileChange(path=filename, status="deleted", status_code="D ")
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
for file_path in status.staged["modify"]: # type: ignore
|
|
396
|
-
filename = (
|
|
397
|
-
file_path.decode("utf-8")
|
|
398
|
-
if isinstance(file_path, bytes)
|
|
399
|
-
else str(file_path)
|
|
400
|
-
)
|
|
401
|
-
files.append(
|
|
402
|
-
GitFileChange(path=filename, status="modified", status_code="M ")
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
# Add unstaged files
|
|
406
|
-
for file_path in status.unstaged: # type: ignore
|
|
407
|
-
filename = (
|
|
408
|
-
file_path.decode("utf-8")
|
|
409
|
-
if isinstance(file_path, bytes)
|
|
410
|
-
else str(file_path)
|
|
411
|
-
)
|
|
412
|
-
files.append(
|
|
413
|
-
GitFileChange(path=filename, status="modified", status_code=" M")
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
# Add untracked files
|
|
417
|
-
for file_path in status.untracked: # type: ignore
|
|
418
|
-
filename = (
|
|
419
|
-
file_path.decode("utf-8")
|
|
420
|
-
if isinstance(file_path, bytes)
|
|
421
|
-
else str(file_path)
|
|
422
|
-
)
|
|
423
|
-
files.append(
|
|
424
|
-
GitFileChange(path=filename, status="untracked", status_code="??")
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
return files
|
|
428
|
-
except Exception:
|
|
429
|
-
return []
|
|
430
|
-
|
|
431
|
-
def has_uncommitted_changes(self) -> bool:
|
|
432
|
-
"""Check if there are uncommitted changes"""
|
|
433
|
-
return len(self.get_changed_files()) > 0
|
|
434
|
-
|
|
435
|
-
def get_repository_status(self) -> GitStatus:
|
|
436
|
-
"""Get comprehensive repository status"""
|
|
437
|
-
if not self.is_git_available():
|
|
438
|
-
return GitStatus(available=False)
|
|
439
|
-
|
|
440
|
-
git_root = self.find_git_root()
|
|
441
|
-
if (
|
|
442
|
-
git_root is not None
|
|
443
|
-
and git_root.resolve() != self.working_directory.resolve() # type: ignore
|
|
444
|
-
):
|
|
445
|
-
return GitStatus(available=False)
|
|
446
|
-
|
|
447
|
-
original_working_dir = None
|
|
448
|
-
if self.is_git_repository():
|
|
449
|
-
git_root = self.working_directory
|
|
450
|
-
elif self.find_git_root():
|
|
451
|
-
git_root = self.find_git_root()
|
|
452
|
-
original_working_dir = self.working_directory
|
|
453
|
-
self.working_directory = git_root
|
|
454
|
-
self._try_load_repo()
|
|
455
|
-
elif self.init_repository():
|
|
456
|
-
git_root = self.working_directory
|
|
457
|
-
else:
|
|
458
|
-
return GitStatus(available=False)
|
|
459
|
-
|
|
460
|
-
try:
|
|
461
|
-
branch = self.get_current_branch()
|
|
462
|
-
if not branch:
|
|
463
|
-
# Check if we're in detached HEAD state or new repo
|
|
464
|
-
try:
|
|
465
|
-
head_ref = self._repo.refs[b"HEAD"] # type: ignore
|
|
466
|
-
if isinstance(head_ref, bytes) and len(head_ref) == 40: # SHA hash
|
|
467
|
-
branch = f"detached-{head_ref[:8].decode('ascii')}"
|
|
468
|
-
else:
|
|
469
|
-
# For new repositories without commits, default to "main"
|
|
470
|
-
branch = "main"
|
|
471
|
-
except Exception:
|
|
472
|
-
# For new repositories without commits, default to "main"
|
|
473
|
-
branch = "main"
|
|
474
|
-
|
|
475
|
-
branches = self.get_all_branches()
|
|
476
|
-
last_commit = self.get_last_commit()
|
|
477
|
-
changed_files = self.get_changed_files()
|
|
478
|
-
changed_files_with_status = self.get_changed_files_with_status()
|
|
479
|
-
has_changes = len(changed_files) > 0
|
|
480
|
-
|
|
481
|
-
return GitStatus(
|
|
482
|
-
available=True,
|
|
483
|
-
branch=branch,
|
|
484
|
-
branches=branches,
|
|
485
|
-
last_commit=last_commit,
|
|
486
|
-
has_changes=has_changes,
|
|
487
|
-
changed_files=changed_files,
|
|
488
|
-
changed_files_with_status=changed_files_with_status,
|
|
489
|
-
)
|
|
490
|
-
finally:
|
|
491
|
-
if original_working_dir is not None:
|
|
492
|
-
self.working_directory = original_working_dir
|
|
493
|
-
|
|
494
|
-
def checkout_branch(self, branch_name: str) -> bool:
|
|
495
|
-
"""Switch to a different branch"""
|
|
496
|
-
if not self._repo:
|
|
497
|
-
return False
|
|
498
|
-
|
|
499
|
-
try:
|
|
500
|
-
# First try to checkout existing local branch
|
|
501
|
-
local_branch_ref = f"refs/heads/{branch_name}".encode()
|
|
502
|
-
if local_branch_ref in self._repo.refs:
|
|
503
|
-
porcelain.checkout_branch(
|
|
504
|
-
str(self.working_directory), branch_name.encode("utf-8")
|
|
505
|
-
) # type: ignore
|
|
506
|
-
return True
|
|
507
|
-
|
|
508
|
-
# If local branch doesn't exist, try to create from remote
|
|
509
|
-
remote_branch_ref = f"refs/remotes/origin/{branch_name}".encode()
|
|
510
|
-
if remote_branch_ref in self._repo.refs:
|
|
511
|
-
# Create local branch tracking remote
|
|
512
|
-
remote_commit = self._repo.refs[remote_branch_ref]
|
|
513
|
-
self._repo.refs[local_branch_ref] = remote_commit
|
|
514
|
-
porcelain.checkout_branch(
|
|
515
|
-
str(self.working_directory), branch_name.encode("utf-8")
|
|
516
|
-
) # type: ignore
|
|
517
|
-
return True
|
|
518
|
-
|
|
519
|
-
return False
|
|
520
|
-
except Exception:
|
|
521
|
-
return False
|
|
522
|
-
|
|
523
|
-
def checkout_commit(self, commit_hash: str) -> bool:
|
|
524
|
-
"""Switch to a specific commit (detached HEAD state)"""
|
|
525
|
-
if not self._repo:
|
|
526
|
-
return False
|
|
527
|
-
|
|
528
|
-
try:
|
|
529
|
-
# Set HEAD to point directly to the commit (detached HEAD)
|
|
530
|
-
self._repo.refs[b"HEAD"] = commit_hash.encode("utf-8") # type: ignore
|
|
531
|
-
# Reset working directory to match the commit using dulwich's reset_hard equivalent
|
|
532
|
-
porcelain.reset(
|
|
533
|
-
str(self.working_directory), "hard", commit_hash.encode("utf-8")
|
|
534
|
-
) # type: ignore
|
|
535
|
-
return True
|
|
536
|
-
except Exception:
|
|
537
|
-
return False
|
|
538
|
-
|
|
539
|
-
def pull_changes(
|
|
540
|
-
self,
|
|
541
|
-
strategy: str = "merge",
|
|
542
|
-
allow_unrelated: bool = True,
|
|
543
|
-
conflict_resolution: Optional[str] = "theirs",
|
|
544
|
-
) -> bool:
|
|
545
|
-
"""Pull changes from abstra remote repository"""
|
|
546
|
-
if not self._repo:
|
|
547
|
-
return False
|
|
548
|
-
|
|
549
|
-
if not self.has_remote(REMOTE_NAME):
|
|
550
|
-
return False
|
|
551
|
-
|
|
552
|
-
try:
|
|
553
|
-
try:
|
|
554
|
-
if not self._needs_auth():
|
|
555
|
-
porcelain.pull(
|
|
556
|
-
self._repo,
|
|
557
|
-
REMOTE_NAME,
|
|
558
|
-
# Add strategy flags similar to native
|
|
559
|
-
)
|
|
560
|
-
return True
|
|
561
|
-
except Exception:
|
|
562
|
-
pass # Fall through to custom implementation
|
|
563
|
-
|
|
564
|
-
# Custom fetch with auth
|
|
565
|
-
remote_url = self._get_remote_url(REMOTE_NAME)
|
|
566
|
-
if not remote_url:
|
|
567
|
-
return False
|
|
568
|
-
|
|
569
|
-
client = self._create_authenticated_client(remote_url)
|
|
570
|
-
if not client:
|
|
571
|
-
return False
|
|
572
|
-
|
|
573
|
-
try:
|
|
574
|
-
# Perform fetch
|
|
575
|
-
parsed_url = urlparse(remote_url)
|
|
576
|
-
repo_path = parsed_url.path.lstrip("/") if parsed_url.path else ""
|
|
577
|
-
|
|
578
|
-
result = client.fetch(
|
|
579
|
-
repo_path,
|
|
580
|
-
self._repo,
|
|
581
|
-
determine_wants=lambda refs, depth=None: list(refs.values()),
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
# Update remote tracking branches properly
|
|
585
|
-
self._update_remote_tracking_refs(result, REMOTE_NAME)
|
|
586
|
-
|
|
587
|
-
# Get the remote branch we want to merge
|
|
588
|
-
remote_branch_ref = f"refs/remotes/{REMOTE_NAME}/main".encode()
|
|
589
|
-
if remote_branch_ref not in self._repo.refs:
|
|
590
|
-
AbstraLogger.warning("No remote main branch found after fetch")
|
|
591
|
-
return False
|
|
592
|
-
|
|
593
|
-
remote_commit_sha = self._repo.refs[remote_branch_ref]
|
|
594
|
-
current_head = self._repo.head()
|
|
595
|
-
|
|
596
|
-
# Check if we're already up to date
|
|
597
|
-
if current_head == remote_commit_sha:
|
|
598
|
-
AbstraLogger.debug("Already up to date")
|
|
599
|
-
return True
|
|
600
|
-
|
|
601
|
-
# Perform the merge/pull based on conflict resolution
|
|
602
|
-
if conflict_resolution == "theirs":
|
|
603
|
-
# Simply move HEAD to remote commit (equivalent to git reset --hard origin/main)
|
|
604
|
-
self._repo.refs[b"HEAD"] = remote_commit_sha
|
|
605
|
-
self._reset_working_tree_to_head()
|
|
606
|
-
|
|
607
|
-
# Also update the local main branch to track remote
|
|
608
|
-
self._repo.refs[b"refs/heads/main"] = remote_commit_sha
|
|
609
|
-
|
|
610
|
-
AbstraLogger.info(
|
|
611
|
-
f"Successfully pulled changes from {REMOTE_NAME} (using theirs)"
|
|
612
|
-
)
|
|
613
|
-
return True
|
|
614
|
-
|
|
615
|
-
elif conflict_resolution == "ours":
|
|
616
|
-
# Keep local changes, just update remote tracking
|
|
617
|
-
AbstraLogger.info("Kept local changes (using ours)")
|
|
618
|
-
return True
|
|
619
|
-
|
|
620
|
-
else:
|
|
621
|
-
# Default behavior - try fast-forward, otherwise use "theirs"
|
|
622
|
-
if self._is_ancestor(current_head, remote_commit_sha):
|
|
623
|
-
# Fast-forward merge
|
|
624
|
-
self._repo.refs[b"HEAD"] = remote_commit_sha
|
|
625
|
-
self._repo.refs[b"refs/heads/main"] = remote_commit_sha
|
|
626
|
-
self._reset_working_tree_to_head()
|
|
627
|
-
AbstraLogger.info(
|
|
628
|
-
f"Successfully pulled changes from {REMOTE_NAME} (fast-forward)"
|
|
629
|
-
)
|
|
630
|
-
return True
|
|
631
|
-
else:
|
|
632
|
-
# Non-fast-forward, default to "theirs" strategy
|
|
633
|
-
self._repo.refs[b"HEAD"] = remote_commit_sha
|
|
634
|
-
self._repo.refs[b"refs/heads/main"] = remote_commit_sha
|
|
635
|
-
self._reset_working_tree_to_head()
|
|
636
|
-
AbstraLogger.info(
|
|
637
|
-
f"Successfully pulled changes from {REMOTE_NAME}"
|
|
638
|
-
)
|
|
639
|
-
return True
|
|
640
|
-
|
|
641
|
-
except Exception as e:
|
|
642
|
-
AbstraLogger.error(f"Error during fetch/merge: {e}")
|
|
643
|
-
return False
|
|
644
|
-
|
|
645
|
-
except Exception as e:
|
|
646
|
-
AbstraLogger.error(f"Error during git pull: {e}")
|
|
647
|
-
return False
|
|
648
|
-
|
|
649
|
-
def _needs_auth(self) -> bool:
|
|
650
|
-
"""Check if authentication is needed"""
|
|
651
|
-
try:
|
|
652
|
-
remote_url = self._get_remote_url(REMOTE_NAME)
|
|
653
|
-
if not remote_url:
|
|
654
|
-
return False
|
|
655
|
-
config_key = f"http.{remote_url}.extraHeader"
|
|
656
|
-
auth_header = self.get_git_config(config_key)
|
|
657
|
-
return bool(auth_header)
|
|
658
|
-
except Exception:
|
|
659
|
-
return False
|
|
660
|
-
|
|
661
|
-
def _get_remote_url(self, remote_name: str) -> Optional[str]:
|
|
662
|
-
"""Get remote URL"""
|
|
663
|
-
try:
|
|
664
|
-
if not self._repo or not hasattr(self._repo, "get_config"):
|
|
665
|
-
return None
|
|
666
|
-
config = self._repo.get_config()
|
|
667
|
-
remote_url = config.get((b"remote", remote_name.encode("utf-8")), b"url")
|
|
668
|
-
return remote_url.decode("utf-8") if remote_url else None
|
|
669
|
-
except Exception:
|
|
670
|
-
return None
|
|
671
|
-
|
|
672
|
-
def _create_authenticated_client(self, remote_url: str):
|
|
673
|
-
"""Create HTTP client with authentication"""
|
|
674
|
-
try:
|
|
675
|
-
auth_header = None
|
|
676
|
-
config_key = f"http.{remote_url}.extraHeader"
|
|
677
|
-
auth_header = self.get_git_config(config_key)
|
|
678
|
-
|
|
679
|
-
if not auth_header:
|
|
680
|
-
return None
|
|
681
|
-
|
|
682
|
-
parsed_url = urlparse(remote_url)
|
|
683
|
-
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
684
|
-
|
|
685
|
-
# Parse auth header
|
|
686
|
-
if ":" not in auth_header:
|
|
687
|
-
return None
|
|
688
|
-
|
|
689
|
-
header_name, header_value = auth_header.split(":", 1)
|
|
690
|
-
custom_headers = {
|
|
691
|
-
header_name.strip(): header_value.strip(),
|
|
692
|
-
"User-Agent": "dulwich/0.24.1",
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
# Create client with custom headers
|
|
696
|
-
pool_manager = urllib3.PoolManager(headers=custom_headers)
|
|
697
|
-
client = HttpGitClient(base_url)
|
|
698
|
-
client.pool_manager = pool_manager
|
|
699
|
-
|
|
700
|
-
return client
|
|
701
|
-
except Exception:
|
|
702
|
-
return None
|
|
703
|
-
|
|
704
|
-
def _update_remote_tracking_refs(self, fetch_result, remote_name: str):
|
|
705
|
-
"""Update remote tracking references after fetch"""
|
|
706
|
-
try:
|
|
707
|
-
if not self._repo or not hasattr(self._repo, "refs"):
|
|
708
|
-
return
|
|
709
|
-
for ref_name, ref_sha in fetch_result.refs.items():
|
|
710
|
-
if ref_name.startswith(b"refs/heads/"):
|
|
711
|
-
branch_name = ref_name[11:]
|
|
712
|
-
remote_ref = f"refs/remotes/{remote_name}/".encode() + branch_name
|
|
713
|
-
self._repo.refs[remote_ref] = ref_sha
|
|
714
|
-
|
|
715
|
-
for symref_name, symref_target in fetch_result.symrefs.items():
|
|
716
|
-
if symref_name == b"HEAD" and symref_target.startswith(b"refs/heads/"):
|
|
717
|
-
branch_name = symref_target[11:]
|
|
718
|
-
remote_head_ref = f"refs/remotes/{remote_name}/HEAD".encode()
|
|
719
|
-
remote_branch_ref = (
|
|
720
|
-
f"refs/remotes/{remote_name}/".encode() + branch_name
|
|
721
|
-
)
|
|
722
|
-
if remote_branch_ref in self._repo.refs:
|
|
723
|
-
self._repo.refs.set_symbolic_ref(
|
|
724
|
-
remote_head_ref, remote_branch_ref
|
|
725
|
-
)
|
|
726
|
-
except Exception as e:
|
|
727
|
-
AbstraLogger.debug(f"Error updating remote tracking refs: {e}")
|
|
728
|
-
|
|
729
|
-
# Remove duplicate _reset_working_tree_to_head method
|
|
730
|
-
|
|
731
|
-
def _is_ancestor(self, ancestor_sha: bytes, descendant_sha: bytes) -> bool:
|
|
732
|
-
"""Check if ancestor_sha is an ancestor of descendant_sha"""
|
|
733
|
-
if not self._repo:
|
|
734
|
-
return False
|
|
735
|
-
|
|
736
|
-
try:
|
|
737
|
-
# Simple check: if we can walk from descendant back to ancestor
|
|
738
|
-
walker = self._repo.get_walker(include=[descendant_sha])
|
|
739
|
-
for entry in walker:
|
|
740
|
-
if entry.commit.id == ancestor_sha:
|
|
741
|
-
return True
|
|
742
|
-
return False
|
|
743
|
-
except Exception:
|
|
744
|
-
return False
|
|
745
|
-
|
|
746
|
-
def _reset_working_tree_to_head(self) -> bool:
|
|
747
|
-
"""Reset working tree to match HEAD"""
|
|
748
|
-
if not self._repo:
|
|
749
|
-
return False
|
|
750
|
-
try:
|
|
751
|
-
# Use dulwich's porcelain reset function for a proper hard reset
|
|
752
|
-
reset(str(self.working_directory), "hard", "HEAD")
|
|
753
|
-
AbstraLogger.debug("Successfully reset working tree to HEAD")
|
|
754
|
-
return True
|
|
755
|
-
except Exception as e:
|
|
756
|
-
AbstraLogger.debug(f"Failed to reset working tree: {e}")
|
|
757
|
-
# Try alternative approach
|
|
758
|
-
try:
|
|
759
|
-
# Get current HEAD commit
|
|
760
|
-
head_sha = self._repo.head() if self._repo else None
|
|
761
|
-
if not head_sha:
|
|
762
|
-
return False
|
|
763
|
-
commit_obj = self._repo[head_sha] if self._repo else None
|
|
764
|
-
if not commit_obj or not isinstance(commit_obj, Commit):
|
|
765
|
-
AbstraLogger.debug("HEAD does not point to a valid commit")
|
|
766
|
-
return False
|
|
767
|
-
# Get the tree object
|
|
768
|
-
tree = (
|
|
769
|
-
self._repo[commit_obj.tree]
|
|
770
|
-
if self._repo and hasattr(commit_obj, "tree")
|
|
771
|
-
else None
|
|
772
|
-
)
|
|
773
|
-
if not tree:
|
|
774
|
-
return False
|
|
775
|
-
# Update index to match tree
|
|
776
|
-
index_path = (
|
|
777
|
-
os.path.join(self._repo.path, "index") if self._repo else "index"
|
|
778
|
-
)
|
|
779
|
-
if (
|
|
780
|
-
self._repo
|
|
781
|
-
and hasattr(self._repo, "object_store")
|
|
782
|
-
and self._repo.object_store is not None
|
|
783
|
-
):
|
|
784
|
-
build_index_from_tree(
|
|
785
|
-
self._repo.path, index_path, self._repo.object_store, tree.id
|
|
786
|
-
)
|
|
787
|
-
else:
|
|
788
|
-
AbstraLogger.debug(
|
|
789
|
-
"object_store is None, cannot build index from tree"
|
|
790
|
-
)
|
|
791
|
-
return False
|
|
792
|
-
AbstraLogger.debug("Successfully updated index")
|
|
793
|
-
return True
|
|
794
|
-
except Exception as e2:
|
|
795
|
-
AbstraLogger.debug(f"Alternative reset also failed: {e2}")
|
|
796
|
-
return False
|
|
797
|
-
|
|
798
|
-
def commit_changes(self, message: str, add_all: bool = True) -> bool:
|
|
799
|
-
"""Commit changes with a message"""
|
|
800
|
-
if not self._repo:
|
|
801
|
-
return False
|
|
802
|
-
|
|
803
|
-
try:
|
|
804
|
-
if add_all:
|
|
805
|
-
# Add all files - porcelain.add expects repo path, not Repo object
|
|
806
|
-
porcelain.add(
|
|
807
|
-
str(self.working_directory), [str(self.working_directory)]
|
|
808
|
-
) # type: ignore
|
|
809
|
-
|
|
810
|
-
# Commit changes - porcelain.commit expects repo path, not Repo object
|
|
811
|
-
porcelain.commit(str(self.working_directory), message.encode("utf-8")) # type: ignore
|
|
812
|
-
return True
|
|
813
|
-
except Exception:
|
|
814
|
-
return False
|
|
815
|
-
|
|
816
|
-
def stash_changes(self, message: str = "WIP") -> bool:
|
|
817
|
-
"""Stash uncommitted changes using Dulwich Stash"""
|
|
818
|
-
if not self._repo:
|
|
819
|
-
return False
|
|
820
|
-
|
|
821
|
-
try:
|
|
822
|
-
# Use Dulwich Stash class
|
|
823
|
-
stash = Stash.from_repo(self._repo) # type: ignore
|
|
824
|
-
stash.push(message=message.encode("utf-8")) # type: ignore
|
|
825
|
-
return True
|
|
826
|
-
except Exception:
|
|
827
|
-
return False
|
|
828
|
-
|
|
829
|
-
def get_remotes(self) -> List[str]:
|
|
830
|
-
"""Get list of remote names"""
|
|
831
|
-
if not self._repo:
|
|
832
|
-
return []
|
|
833
|
-
|
|
834
|
-
if not self._repo:
|
|
835
|
-
return []
|
|
836
|
-
try:
|
|
837
|
-
config = self._repo.get_config() # type: ignore
|
|
838
|
-
remotes = []
|
|
839
|
-
for section_name in config.sections():
|
|
840
|
-
if len(section_name) >= 2 and section_name[0] == b"remote":
|
|
841
|
-
remote_name = section_name[1].decode("utf-8")
|
|
842
|
-
remotes.append(remote_name)
|
|
843
|
-
return remotes
|
|
844
|
-
except Exception as e:
|
|
845
|
-
AbstraLogger.error(f"Error getting remotes: {e}")
|
|
846
|
-
return []
|
|
847
|
-
|
|
848
|
-
def has_remote(self, remote_name: str) -> bool:
|
|
849
|
-
"""Check if a remote exists"""
|
|
850
|
-
remotes = self.get_remotes()
|
|
851
|
-
if not remotes:
|
|
852
|
-
return False
|
|
853
|
-
return remote_name in remotes
|
|
854
|
-
|
|
855
|
-
def add_remote(self, remote_name: str, remote_url: str) -> bool:
|
|
856
|
-
"""Add a remote to the repository"""
|
|
857
|
-
if not self._repo:
|
|
858
|
-
return False
|
|
859
|
-
try:
|
|
860
|
-
config = self._repo.get_config() # type: ignore
|
|
861
|
-
config.set(
|
|
862
|
-
(b"remote", remote_name.encode("utf-8")),
|
|
863
|
-
b"url",
|
|
864
|
-
remote_url.encode("utf-8"),
|
|
865
|
-
)
|
|
866
|
-
config.write_to_path()
|
|
867
|
-
return True
|
|
868
|
-
except Exception as e:
|
|
869
|
-
AbstraLogger.error(f"Error adding remote {remote_name}: {e}")
|
|
870
|
-
return False
|
|
871
|
-
|
|
872
|
-
def set_remote_url(self, remote_name: str, remote_url: str) -> bool:
|
|
873
|
-
"""Set/update the URL for a remote"""
|
|
874
|
-
if not self._repo:
|
|
875
|
-
return False
|
|
876
|
-
|
|
877
|
-
try:
|
|
878
|
-
config = self._repo.get_config() # type: ignore
|
|
879
|
-
config.set(
|
|
880
|
-
(b"remote", remote_name.encode("utf-8")),
|
|
881
|
-
b"url",
|
|
882
|
-
remote_url.encode("utf-8"),
|
|
883
|
-
)
|
|
884
|
-
config.write_to_path()
|
|
885
|
-
return True
|
|
886
|
-
except Exception as e:
|
|
887
|
-
AbstraLogger.error(f"Error setting remote URL for {remote_name}: {e}")
|
|
888
|
-
return False
|
|
889
|
-
|
|
890
|
-
def set_git_config(self, key: str, value: str, local: bool = True) -> bool:
|
|
891
|
-
"""Set a git configuration value"""
|
|
892
|
-
if not self._repo:
|
|
893
|
-
return False
|
|
894
|
-
|
|
895
|
-
try:
|
|
896
|
-
config = self._repo.get_config() # type: ignore
|
|
897
|
-
|
|
898
|
-
# Handle special case for http.<url>.* configs
|
|
899
|
-
if key.startswith("http.") and key.count(".") >= 2:
|
|
900
|
-
# For keys like "http.https://example.com/repo.git.extraHeader"
|
|
901
|
-
# We need to find the last dot that separates the option from the URL
|
|
902
|
-
# Split from the right to separate option from URL
|
|
903
|
-
http_prefix = "http."
|
|
904
|
-
remaining = key[len(http_prefix) :] # Remove "http." prefix
|
|
905
|
-
|
|
906
|
-
# Find the last dot to separate option from URL
|
|
907
|
-
last_dot_index = remaining.rfind(".")
|
|
908
|
-
if last_dot_index > 0:
|
|
909
|
-
url_part = remaining[:last_dot_index]
|
|
910
|
-
option_part = remaining[last_dot_index + 1 :]
|
|
911
|
-
|
|
912
|
-
config.set(
|
|
913
|
-
(b"http", url_part.encode("utf-8")),
|
|
914
|
-
option_part.encode("utf-8"),
|
|
915
|
-
value.encode("utf-8"),
|
|
916
|
-
)
|
|
917
|
-
config.write_to_path()
|
|
918
|
-
return True
|
|
919
|
-
|
|
920
|
-
# Handle normal config keys like "user.name", "core.editor"
|
|
921
|
-
parts = key.split(".", 1)
|
|
922
|
-
if len(parts) == 2:
|
|
923
|
-
section, option = parts
|
|
924
|
-
config.set(
|
|
925
|
-
(section.encode("utf-8"),),
|
|
926
|
-
option.encode("utf-8"),
|
|
927
|
-
value.encode("utf-8"),
|
|
928
|
-
)
|
|
929
|
-
config.write_to_path()
|
|
930
|
-
return True
|
|
931
|
-
return False
|
|
932
|
-
except Exception as e:
|
|
933
|
-
AbstraLogger.error(f"Error setting git config {key}: {e}")
|
|
934
|
-
return False
|
|
935
|
-
|
|
936
|
-
def get_git_config(self, key: str, local: bool = True) -> Optional[str]:
|
|
937
|
-
"""Get a git configuration value"""
|
|
938
|
-
if not self._repo:
|
|
939
|
-
return None
|
|
940
|
-
|
|
941
|
-
try:
|
|
942
|
-
config = self._repo.get_config() # type: ignore
|
|
943
|
-
|
|
944
|
-
# Handle special case for http.<url>.* configs
|
|
945
|
-
if key.startswith("http.") and key.count(".") >= 2:
|
|
946
|
-
# For keys like "http.https://example.com/repo.git.extraHeader"
|
|
947
|
-
# We need to find the last dot that separates the option from the URL
|
|
948
|
-
# Split from the right to separate option from URL
|
|
949
|
-
http_prefix = "http."
|
|
950
|
-
remaining = key[len(http_prefix) :] # Remove "http." prefix
|
|
951
|
-
|
|
952
|
-
# Find the last dot to separate option from URL
|
|
953
|
-
last_dot_index = remaining.rfind(".")
|
|
954
|
-
if last_dot_index > 0:
|
|
955
|
-
url_part = remaining[:last_dot_index]
|
|
956
|
-
option_part = remaining[last_dot_index + 1 :]
|
|
957
|
-
|
|
958
|
-
try:
|
|
959
|
-
value = config.get(
|
|
960
|
-
(b"http", url_part.encode("utf-8")),
|
|
961
|
-
option_part.encode("utf-8"),
|
|
962
|
-
)
|
|
963
|
-
return value.decode("utf-8") if value else None
|
|
964
|
-
except KeyError:
|
|
965
|
-
return None
|
|
966
|
-
|
|
967
|
-
# Handle normal config keys like "user.name", "core.editor"
|
|
968
|
-
parts = key.split(".", 1)
|
|
969
|
-
if len(parts) == 2:
|
|
970
|
-
section, option = parts
|
|
971
|
-
try:
|
|
972
|
-
value = config.get(
|
|
973
|
-
(section.encode("utf-8"),), option.encode("utf-8")
|
|
974
|
-
)
|
|
975
|
-
return value.decode("utf-8") if value else None
|
|
976
|
-
except KeyError:
|
|
977
|
-
return None
|
|
978
|
-
return None
|
|
979
|
-
except Exception:
|
|
980
|
-
return None
|
|
981
|
-
|
|
982
|
-
def push_and_deploy(self, branch: str = "main") -> bool:
|
|
983
|
-
"""Deploy to Abstra remote (push to abstra remote)"""
|
|
984
|
-
if not self._repo:
|
|
985
|
-
return False
|
|
986
|
-
if not self.has_remote(REMOTE_NAME):
|
|
987
|
-
return False
|
|
988
|
-
try:
|
|
989
|
-
branch_name = branch if branch else "main"
|
|
990
|
-
branch_ref = f"refs/heads/{branch_name}".encode()
|
|
991
|
-
if not hasattr(self._repo, "refs") or branch_ref not in self._repo.refs:
|
|
992
|
-
return False
|
|
993
|
-
porcelain.push(
|
|
994
|
-
str(self.working_directory),
|
|
995
|
-
REMOTE_NAME,
|
|
996
|
-
refspecs=[f"{branch_name}:{branch_name}"],
|
|
997
|
-
)
|
|
998
|
-
return True
|
|
999
|
-
except Exception:
|
|
1000
|
-
return False
|
|
1001
|
-
|
|
1002
|
-
def revert_commit(self, commit_hash: str) -> bool:
|
|
1003
|
-
"""Reset working directory to match a previous commit and create a new commit with that content"""
|
|
1004
|
-
if not self._repo:
|
|
1005
|
-
return False
|
|
1006
|
-
if not commit_hash or not commit_hash.strip():
|
|
1007
|
-
return False
|
|
1008
|
-
if self.has_uncommitted_changes():
|
|
1009
|
-
return False
|
|
1010
|
-
try:
|
|
1011
|
-
# Verify the commit exists
|
|
1012
|
-
try:
|
|
1013
|
-
if not hasattr(self._repo, "__getitem__"):
|
|
1014
|
-
return False
|
|
1015
|
-
self._repo[commit_hash.encode()]
|
|
1016
|
-
except KeyError:
|
|
1017
|
-
return False
|
|
1018
|
-
# Get current HEAD
|
|
1019
|
-
try:
|
|
1020
|
-
current_head = (
|
|
1021
|
-
self._repo.head() if hasattr(self._repo, "head") else None
|
|
1022
|
-
)
|
|
1023
|
-
if not current_head:
|
|
1024
|
-
return False
|
|
1025
|
-
self._repo[current_head]
|
|
1026
|
-
except Exception:
|
|
1027
|
-
return False
|
|
1028
|
-
# Step 1: Reset working directory to target commit state
|
|
1029
|
-
porcelain.reset(str(self.working_directory), "hard", commit_hash.encode())
|
|
1030
|
-
# Step 2: Reset HEAD back to original position (but keep working directory)
|
|
1031
|
-
if hasattr(self._repo, "refs"):
|
|
1032
|
-
self._repo.refs[b"HEAD"] = current_head
|
|
1033
|
-
# Step 3: Add all changes and create new commit
|
|
1034
|
-
porcelain.add(str(self.working_directory), [str(self.working_directory)])
|
|
1035
|
-
commit_message = f"Restore content from commit {commit_hash[:8]}"
|
|
1036
|
-
porcelain.commit(
|
|
1037
|
-
str(self.working_directory), commit_message.encode("utf-8")
|
|
1038
|
-
)
|
|
1039
|
-
return True
|
|
1040
|
-
except Exception:
|
|
1041
|
-
try:
|
|
1042
|
-
porcelain.reset(str(self.working_directory), "hard")
|
|
1043
|
-
except Exception:
|
|
1044
|
-
pass
|
|
1045
|
-
return False
|
|
1046
|
-
|
|
1047
|
-
def check_merge_conflicts(self, remote_commit: str) -> bool:
|
|
1048
|
-
"""Check if merging with remote commit would cause conflicts"""
|
|
1049
|
-
if not self._repo:
|
|
1050
|
-
return False
|
|
1051
|
-
|
|
1052
|
-
if not remote_commit:
|
|
1053
|
-
return False
|
|
1054
|
-
|
|
1055
|
-
try:
|
|
1056
|
-
try:
|
|
1057
|
-
current_commit_id = self._repo.head()
|
|
1058
|
-
except Exception:
|
|
1059
|
-
return False
|
|
1060
|
-
|
|
1061
|
-
try:
|
|
1062
|
-
if isinstance(remote_commit, str):
|
|
1063
|
-
remote_commit_bytes = remote_commit.encode()
|
|
1064
|
-
else:
|
|
1065
|
-
remote_commit_bytes = remote_commit
|
|
1066
|
-
except KeyError:
|
|
1067
|
-
# If remote commit doesn't exist locally, this is equivalent to merge-base failing
|
|
1068
|
-
# Native git returns True (conflicts) in this case
|
|
1069
|
-
return True
|
|
1070
|
-
|
|
1071
|
-
# Try to find merge base (equivalent to git merge-base)
|
|
1072
|
-
try:
|
|
1073
|
-
merge_base_id = self._find_merge_base_dulwich(
|
|
1074
|
-
current_commit_id, remote_commit_bytes
|
|
1075
|
-
)
|
|
1076
|
-
if merge_base_id is None:
|
|
1077
|
-
return True
|
|
1078
|
-
except Exception:
|
|
1079
|
-
return True
|
|
1080
|
-
|
|
1081
|
-
# Now run merge-tree equivalent (equivalent to git merge-tree)
|
|
1082
|
-
try:
|
|
1083
|
-
has_conflicts = self._simulate_merge_tree_dulwich(
|
|
1084
|
-
merge_base_id, current_commit_id, remote_commit_bytes
|
|
1085
|
-
)
|
|
1086
|
-
return has_conflicts
|
|
1087
|
-
except Exception:
|
|
1088
|
-
return False
|
|
1089
|
-
|
|
1090
|
-
except Exception:
|
|
1091
|
-
return False
|
|
1092
|
-
|
|
1093
|
-
def _find_merge_base_dulwich(self, commit1_id, commit2_id):
|
|
1094
|
-
"""Find merge base between two commits - equivalent to git merge-base"""
|
|
1095
|
-
try:
|
|
1096
|
-
if not self._repo:
|
|
1097
|
-
return None
|
|
1098
|
-
if isinstance(commit2_id, str):
|
|
1099
|
-
commit2_id = commit2_id.encode()
|
|
1100
|
-
from collections import deque
|
|
1101
|
-
|
|
1102
|
-
visited1 = set()
|
|
1103
|
-
queue1 = deque([commit1_id])
|
|
1104
|
-
while queue1:
|
|
1105
|
-
commit_id = queue1.popleft()
|
|
1106
|
-
if commit_id in visited1:
|
|
1107
|
-
continue
|
|
1108
|
-
visited1.add(commit_id)
|
|
1109
|
-
try:
|
|
1110
|
-
commit_obj = (
|
|
1111
|
-
self._repo[commit_id]
|
|
1112
|
-
if hasattr(self._repo, "__getitem__")
|
|
1113
|
-
else None
|
|
1114
|
-
)
|
|
1115
|
-
if (
|
|
1116
|
-
commit_obj
|
|
1117
|
-
and isinstance(commit_obj, Commit)
|
|
1118
|
-
and hasattr(commit_obj, "parents")
|
|
1119
|
-
):
|
|
1120
|
-
queue1.extend(commit_obj.parents)
|
|
1121
|
-
except Exception as e:
|
|
1122
|
-
print(f"Error accessing commit {commit_id}: {e}")
|
|
1123
|
-
continue
|
|
1124
|
-
visited2 = set()
|
|
1125
|
-
queue2 = deque([commit2_id])
|
|
1126
|
-
while queue2:
|
|
1127
|
-
commit_id = queue2.popleft()
|
|
1128
|
-
if commit_id in visited2:
|
|
1129
|
-
continue
|
|
1130
|
-
visited2.add(commit_id)
|
|
1131
|
-
if commit_id in visited1:
|
|
1132
|
-
return commit_id
|
|
1133
|
-
try:
|
|
1134
|
-
commit_obj = (
|
|
1135
|
-
self._repo[commit_id]
|
|
1136
|
-
if hasattr(self._repo, "__getitem__")
|
|
1137
|
-
else None
|
|
1138
|
-
)
|
|
1139
|
-
if (
|
|
1140
|
-
commit_obj
|
|
1141
|
-
and isinstance(commit_obj, Commit)
|
|
1142
|
-
and hasattr(commit_obj, "parents")
|
|
1143
|
-
):
|
|
1144
|
-
queue2.extend(commit_obj.parents)
|
|
1145
|
-
except Exception as e:
|
|
1146
|
-
print(f"Error accessing commit {commit_id}: {e}")
|
|
1147
|
-
continue
|
|
1148
|
-
return None
|
|
1149
|
-
except Exception as e:
|
|
1150
|
-
print(f"Error in _find_merge_base_dulwich: {e}")
|
|
1151
|
-
return None
|
|
1152
|
-
|
|
1153
|
-
def _simulate_merge_tree_dulwich(self, merge_base_id, current_id, remote_id):
|
|
1154
|
-
"""Simulate git merge-tree to detect conflicts"""
|
|
1155
|
-
try:
|
|
1156
|
-
if not self._repo:
|
|
1157
|
-
return False
|
|
1158
|
-
if isinstance(remote_id, str):
|
|
1159
|
-
remote_id = remote_id.encode()
|
|
1160
|
-
base_commit = (
|
|
1161
|
-
self._repo[merge_base_id]
|
|
1162
|
-
if hasattr(self._repo, "__getitem__")
|
|
1163
|
-
else None
|
|
1164
|
-
)
|
|
1165
|
-
current_commit = (
|
|
1166
|
-
self._repo[current_id] if hasattr(self._repo, "__getitem__") else None
|
|
1167
|
-
)
|
|
1168
|
-
remote_commit = (
|
|
1169
|
-
self._repo[remote_id] if hasattr(self._repo, "__getitem__") else None
|
|
1170
|
-
)
|
|
1171
|
-
if not base_commit or not current_commit or not remote_commit:
|
|
1172
|
-
return False
|
|
1173
|
-
base_tree = (
|
|
1174
|
-
self._repo[base_commit.tree]
|
|
1175
|
-
if base_commit
|
|
1176
|
-
and isinstance(base_commit, Commit)
|
|
1177
|
-
and hasattr(base_commit, "tree")
|
|
1178
|
-
and hasattr(self._repo, "__getitem__")
|
|
1179
|
-
else None
|
|
1180
|
-
)
|
|
1181
|
-
current_tree = (
|
|
1182
|
-
self._repo[current_commit.tree]
|
|
1183
|
-
if current_commit
|
|
1184
|
-
and isinstance(current_commit, Commit)
|
|
1185
|
-
and hasattr(current_commit, "tree")
|
|
1186
|
-
and hasattr(self._repo, "__getitem__")
|
|
1187
|
-
else None
|
|
1188
|
-
)
|
|
1189
|
-
remote_tree = (
|
|
1190
|
-
self._repo[remote_commit.tree]
|
|
1191
|
-
if remote_commit
|
|
1192
|
-
and isinstance(remote_commit, Commit)
|
|
1193
|
-
and hasattr(remote_commit, "tree")
|
|
1194
|
-
and hasattr(self._repo, "__getitem__")
|
|
1195
|
-
else None
|
|
1196
|
-
)
|
|
1197
|
-
if not base_tree or not current_tree or not remote_tree:
|
|
1198
|
-
return False
|
|
1199
|
-
if base_tree.id == current_tree.id == remote_tree.id:
|
|
1200
|
-
return False
|
|
1201
|
-
if current_tree.id == base_tree.id:
|
|
1202
|
-
return False
|
|
1203
|
-
if remote_tree.id == base_tree.id:
|
|
1204
|
-
return False
|
|
1205
|
-
if current_tree.id == remote_tree.id:
|
|
1206
|
-
return False
|
|
1207
|
-
return self._analyze_tree_conflicts(base_tree, current_tree, remote_tree)
|
|
1208
|
-
except Exception as e:
|
|
1209
|
-
print(f"Error in _simulate_merge_tree_dulwich: {e}")
|
|
1210
|
-
return False
|
|
1211
|
-
|
|
1212
|
-
def _analyze_tree_conflicts(self, base_tree, current_tree, remote_tree):
|
|
1213
|
-
"""Analyze trees for conflicts like git merge-tree does"""
|
|
1214
|
-
try:
|
|
1215
|
-
# Get all file entries from all three trees
|
|
1216
|
-
base_entries = self._get_tree_entries(base_tree)
|
|
1217
|
-
current_entries = self._get_tree_entries(current_tree)
|
|
1218
|
-
remote_entries = self._get_tree_entries(remote_tree)
|
|
1219
|
-
|
|
1220
|
-
# Get all unique file paths
|
|
1221
|
-
all_paths = (
|
|
1222
|
-
set(base_entries.keys())
|
|
1223
|
-
| set(current_entries.keys())
|
|
1224
|
-
| set(remote_entries.keys())
|
|
1225
|
-
)
|
|
1226
|
-
|
|
1227
|
-
for path in all_paths:
|
|
1228
|
-
base_entry = base_entries.get(path)
|
|
1229
|
-
current_entry = current_entries.get(path)
|
|
1230
|
-
remote_entry = remote_entries.get(path)
|
|
1231
|
-
# Only check for conflicts if all entries are not None and are tuples
|
|
1232
|
-
if (
|
|
1233
|
-
(base_entry is not None and isinstance(base_entry, tuple))
|
|
1234
|
-
or (current_entry is not None and isinstance(current_entry, tuple))
|
|
1235
|
-
or (remote_entry is not None and isinstance(remote_entry, tuple))
|
|
1236
|
-
):
|
|
1237
|
-
try:
|
|
1238
|
-
if self._has_file_conflict(
|
|
1239
|
-
base_entry, current_entry, remote_entry
|
|
1240
|
-
):
|
|
1241
|
-
return True
|
|
1242
|
-
except TypeError:
|
|
1243
|
-
continue
|
|
1244
|
-
|
|
1245
|
-
return False
|
|
1246
|
-
|
|
1247
|
-
except Exception as e:
|
|
1248
|
-
print(f"Error in _analyze_tree_conflicts: {e}")
|
|
1249
|
-
return True # Conservative: assume conflicts on error
|
|
1250
|
-
|
|
1251
|
-
def _get_tree_entries(self, tree):
|
|
1252
|
-
"""Get all file entries from a tree recursively"""
|
|
1253
|
-
entries = {}
|
|
1254
|
-
|
|
1255
|
-
def walk_tree(tree_obj, prefix=""):
|
|
1256
|
-
try:
|
|
1257
|
-
if not tree_obj or not hasattr(tree_obj, "items"):
|
|
1258
|
-
return
|
|
1259
|
-
for name, mode, sha in tree_obj.items():
|
|
1260
|
-
path = prefix + name.decode("utf-8", errors="replace")
|
|
1261
|
-
if mode & 0o040000: # Directory
|
|
1262
|
-
try:
|
|
1263
|
-
subtree = (
|
|
1264
|
-
self._repo[sha]
|
|
1265
|
-
if self._repo and hasattr(self._repo, "__getitem__")
|
|
1266
|
-
else None
|
|
1267
|
-
)
|
|
1268
|
-
if subtree is not None:
|
|
1269
|
-
walk_tree(subtree, path + "/")
|
|
1270
|
-
except Exception as e:
|
|
1271
|
-
print(f"DEBUG: Error accessing subtree {sha}: {e}")
|
|
1272
|
-
else: # File
|
|
1273
|
-
entries[path] = (mode, sha)
|
|
1274
|
-
except Exception as e:
|
|
1275
|
-
print(f"DEBUG: Error accessing tree {tree_obj}: {e}")
|
|
1276
|
-
|
|
1277
|
-
walk_tree(tree)
|
|
1278
|
-
return entries
|
|
1279
|
-
|
|
1280
|
-
def _has_file_conflict(self, base_entry, current_entry, remote_entry):
|
|
1281
|
-
"""Check if a file has conflicts between the three versions"""
|
|
1282
|
-
# If all three are identical or missing, no conflict
|
|
1283
|
-
if base_entry == current_entry == remote_entry:
|
|
1284
|
-
return False
|
|
1285
|
-
|
|
1286
|
-
# If file doesn't exist in base
|
|
1287
|
-
if base_entry is None:
|
|
1288
|
-
if current_entry is None or remote_entry is None:
|
|
1289
|
-
return False # One side didn't add the file
|
|
1290
|
-
# Both sides added - conflict if different content
|
|
1291
|
-
return current_entry[1] != remote_entry[1] # Compare SHA
|
|
1292
|
-
|
|
1293
|
-
# File exists in base
|
|
1294
|
-
if current_entry is None and remote_entry is None:
|
|
1295
|
-
return False # Both sides deleted - no conflict
|
|
1296
|
-
|
|
1297
|
-
if current_entry is None:
|
|
1298
|
-
return False # Only remote changed - no conflict
|
|
1299
|
-
|
|
1300
|
-
if remote_entry is None:
|
|
1301
|
-
return False # Only current changed - no conflict
|
|
1302
|
-
|
|
1303
|
-
# All three versions exist - check for conflicts
|
|
1304
|
-
current_changed = current_entry[1] != base_entry[1]
|
|
1305
|
-
remote_changed = remote_entry[1] != base_entry[1]
|
|
1306
|
-
|
|
1307
|
-
if not current_changed or not remote_changed:
|
|
1308
|
-
return False # At most one side changed
|
|
1309
|
-
|
|
1310
|
-
# Both sides changed - conflict if they changed differently
|
|
1311
|
-
return current_entry[1] != remote_entry[1]
|
|
1312
|
-
|
|
1313
|
-
def get_ahead_behind_count(
|
|
1314
|
-
self, local_commit: str, remote_commit: str
|
|
1315
|
-
) -> Tuple[int, int]:
|
|
1316
|
-
"""Calculate ahead/behind count between local and remote commits"""
|
|
1317
|
-
if not self._repo:
|
|
1318
|
-
return (0, 0)
|
|
1319
|
-
|
|
1320
|
-
try:
|
|
1321
|
-
# Verify both commits exist
|
|
1322
|
-
try:
|
|
1323
|
-
self._repo[local_commit.encode()]
|
|
1324
|
-
self._repo[remote_commit.encode()]
|
|
1325
|
-
except KeyError:
|
|
1326
|
-
# If remote commit doesn't exist locally, we're behind
|
|
1327
|
-
# This matches the native implementation behavior
|
|
1328
|
-
return (1, 1) # Indicates divergence requiring merge/rebase
|
|
1329
|
-
|
|
1330
|
-
# Count commits that local has but remote doesn't (ahead)
|
|
1331
|
-
try:
|
|
1332
|
-
# Get commits reachable from local but not from remote
|
|
1333
|
-
ahead_walker = self._repo.get_walker(
|
|
1334
|
-
include=[local_commit.encode()], exclude=[remote_commit.encode()]
|
|
1335
|
-
)
|
|
1336
|
-
ahead_count = len(list(ahead_walker))
|
|
1337
|
-
except Exception:
|
|
1338
|
-
ahead_count = 0
|
|
1339
|
-
|
|
1340
|
-
# Count commits that remote has but local doesn't (behind)
|
|
1341
|
-
try:
|
|
1342
|
-
# Get commits reachable from remote but not from local
|
|
1343
|
-
behind_walker = self._repo.get_walker(
|
|
1344
|
-
include=[remote_commit.encode()], exclude=[local_commit.encode()]
|
|
1345
|
-
)
|
|
1346
|
-
behind_count = len(list(behind_walker))
|
|
1347
|
-
except Exception:
|
|
1348
|
-
behind_count = 0
|
|
1349
|
-
|
|
1350
|
-
return (ahead_count, behind_count)
|
|
1351
|
-
|
|
1352
|
-
except Exception:
|
|
1353
|
-
return (0, 0)
|