commit 8d053238ed7ec9d9962d3a511e42bf7e071a5a0d Author: xiaoyan Date: Wed Mar 29 17:28:46 2023 +0800 feat: 初次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bdeb92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +build \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..818f410 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/../../../../../:\Learn\Python\PythonTest\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/PythonTest.iml b/.idea/PythonTest.iml new file mode 100644 index 0000000..f5fb9cc --- /dev/null +++ b/.idea/PythonTest.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..73199ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1fb6258 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MergeOMDB.py b/MergeOMDB.py new file mode 100644 index 0000000..401524b --- /dev/null +++ b/MergeOMDB.py @@ -0,0 +1,95 @@ +# coding:utf-8 +# 合并指定目录下的omdb(sqlite)数据 + +import os +import sys +import json +import sqlite3 + + +# 定义遍历目录的函数 +def traverse_dir(path): + fileList = list() + for root, dirs, files in os.walk(path): + for file in files: + if str(file).endswith(".omdb"): + # 文件的完整路径 + file_path = os.path.join(root, file) + # 处理文件,例如读取文件内容等 + print(file_path) + fileList.append(file_path) + return fileList + + +# 打开配置文件,读取用户配置的 +def openConfigJson(path): + # 读取json配置,获取要抽取的表名 + with open(path, "r") as f: + configMap = json.load(f) + return configMap + + +# 按照tableList中指定的表名合并多个源数据库到指定目标数据库中 +def mergeSqliteData(originSqliteList, destSqlite, tableList): + destConn = sqlite3.connect(destSqlite) + destCursor = destConn.cursor() + + for originSqlite in originSqliteList: + originConn = sqlite3.connect(originSqlite) + originCursor = originConn.cursor() + # 从源数据库中遍历取出表list中的数据 + for table in tableList: + # 检查目标数据库中是否存在指定的表 + containsTable = destCursor.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'" % (table)).fetchall() + if not containsTable or len(containsTable) <= 0: + # 复制表结构 + originCursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='%s'" % (table)) + createTableSql = originCursor.fetchone()[0] + destCursor.execute(createTableSql) + destConn.commit() + + originCursor.execute("Select * From " + table) + # 获取到源数据库中该表的所有数据 + originData = originCursor.fetchall() + # 获取一行数据中包含多少列,以此动态设置sql语句中的?个数 + if originData and len(originData)>0: + num_cols = len(originData[0]) + placeholders = ",".join(["?"] * num_cols) + for row in originData: + destCursor.execute("INSERT INTO "+table+" VALUES ({})".format(placeholders), row) + + print("{}数据已导入!".format(originSqlite)) + originCursor.close() + originConn.close() + destConn.commit() + destCursor.close() + destConn.close() + + +if __name__ == '__main__': + params = sys.argv[1:] # 截取参数 + if params: + if not params[0]: + print("请输入要合并的omdb数据的文件夹") + raise AttributeError("请输入要合并的omdb数据的文件夹") + # 获取导出文件的表配置 + jsonPath = params[0] + "/config.json" + if not os.path.exists(jsonPath): + raise AttributeError("指定目录下缺少config.json配置文件") + omdbDir = params[0] + originSqliteList = traverse_dir(omdbDir) # 获取到所有的omdb数据库的路径 + + tableNameList = list() + configMap = openConfigJson(jsonPath) + if configMap["tables"] and len(configMap["tables"]) > 0: + for tableName in set(configMap["tables"]): + tableNameList.append(tableName) + print(tableNameList) + else: + raise AttributeError("config.json文件中没有配置抽取数据的表名") + + # 开始分别连接Sqlite数据库,按照指定表名合并数据 + mergeSqliteData(originSqliteList, params[0]+"/output.sqlite", tableNameList) + else: + raise AttributeError("缺少参数:请输入要合并的omdb数据的文件夹") diff --git a/autoGit.py b/autoGit.py new file mode 100644 index 0000000..59b0065 --- /dev/null +++ b/autoGit.py @@ -0,0 +1,7 @@ +import git +import os + +if __name__ == '__main__': + gitPath = os.path.join("test", "git") + if os.path.exists(gitPath) is False: + print(os.makedirs("d:\\test\\git")) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..bd887b6 --- /dev/null +++ b/main.py @@ -0,0 +1,22 @@ +# This is a sample Python script. + +# Press Alt+Shift+X to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. +import json + + +def print_hi(name): + str = '[{1001,[{0,-1,"",100}],[{"alice",100}]},{1002,[{1001,0,"alice",25}],[{"bob",25},{"alice",75}]},{1003,[{1002,1,"alice",5},{1002,0,"bob",20}],[{"eve",25},{"alice",70},{"bob",5}]}]' + getObject(str) + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Ctrl+Shift+B to toggle the breakpoint. + +def getObject(str): + jsonArray = json.dumps(str) + return jsonArray + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/AUTHORS b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/AUTHORS new file mode 100644 index 0000000..55d6818 --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/AUTHORS @@ -0,0 +1,48 @@ +GitPython was originally written by Michael Trier. +GitPython 0.2 was partially (re)written by Sebastian Thiel, based on 0.1.6 and git-dulwich. + +Contributors are: + +-Michael Trier +-Alan Briolat +-Florian Apolloner +-David Aguilar +-Jelmer Vernooij +-Steve Frécinaux +-Kai Lautaportti +-Paul Sowden +-Sebastian Thiel +-Jonathan Chu +-Vincent Driessen +-Phil Elson +-Bernard `Guyzmo` Pratz +-Timothy B. Hartman +-Konstantin Popov +-Peter Jones +-Anson Mansfield +-Ken Odegard +-Alexis Horgix Chotard +-Piotr Babij +-Mikuláš Poul +-Charles Bouchard-Légaré +-Yaroslav Halchenko +-Tim Swast +-William Luc Ritchie +-David Host +-A. Jesse Jiryu Davis +-Steven Whitman +-Stefan Stancu +-César Izurieta +-Arthur Milchior +-Anil Khatri +-JJ Graham +-Ben Thayer +-Dries Kennes +-Pratik Anurag +-Harmon +-Liam Beguin +-Ram Rachum +-Alba Mendez +-Robert Westman +-Hugo van Kemenade +Portions derived from other open source works and are clearly marked. diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/INSTALLER b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/LICENSE b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/LICENSE new file mode 100644 index 0000000..5a9a6f8 --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/LICENSE @@ -0,0 +1,30 @@ +Copyright (C) 2008, 2009 Michael Trier and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +* Neither the name of the GitPython project nor the names of +its contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/METADATA b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/METADATA new file mode 100644 index 0000000..8a04eba --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/METADATA @@ -0,0 +1,33 @@ +Metadata-Version: 2.1 +Name: GitPython +Version: 3.1.27 +Summary: GitPython is a python library used to interact with Git repositories +Home-page: https://github.com/gitpython-developers/GitPython +Author: Sebastian Thiel, Michael Trier +Author-email: byronimo@gmail.com, mtrier@gmail.com +License: BSD +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Typing :: Typed +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +License-File: AUTHORS +Requires-Dist: gitdb (<5,>=4.0.1) +Requires-Dist: typing-extensions (>=3.7.4.3) ; python_version < "3.8" + +GitPython is a python library used to interact with Git repositories + diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/RECORD b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/RECORD new file mode 100644 index 0000000..e588b40 --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/RECORD @@ -0,0 +1,83 @@ +GitPython-3.1.27.dist-info/AUTHORS,sha256=vjnd09wZL3p1v8gB5lsk4nj-2nDyHcZEzY_MKrZQyco,1936 +GitPython-3.1.27.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +GitPython-3.1.27.dist-info/LICENSE,sha256=_WV__CzvY9JceMq3gI1BTdA6KC5jiTSR_RHDL5i-Z_s,1521 +GitPython-3.1.27.dist-info/METADATA,sha256=h9Z-ZVEP5r7UES4v0wOH-dyhekIRG0qBBg_7VX5zAFc,1289 +GitPython-3.1.27.dist-info/RECORD,, +GitPython-3.1.27.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +GitPython-3.1.27.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +GitPython-3.1.27.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4 +git/__init__.py,sha256=LIqmpO0UYXoia_6ymWif0fdDpVN1apyh-yuzD1Qmkog,2500 +git/__pycache__/__init__.cpython-310.pyc,, +git/__pycache__/cmd.cpython-310.pyc,, +git/__pycache__/compat.cpython-310.pyc,, +git/__pycache__/config.cpython-310.pyc,, +git/__pycache__/db.cpython-310.pyc,, +git/__pycache__/diff.cpython-310.pyc,, +git/__pycache__/exc.cpython-310.pyc,, +git/__pycache__/remote.cpython-310.pyc,, +git/__pycache__/types.cpython-310.pyc,, +git/__pycache__/util.cpython-310.pyc,, +git/cmd.py,sha256=8QI5KptzHzAEGzfMg2FtQrPaggMAzv_YKwF4-rGhJRI,51897 +git/compat.py,sha256=A__j0NDexK9vm-IP59CveY7V_Epef9Km4wH94nykvGs,2244 +git/config.py,sha256=ABfTzzFy82NkwTKHreMbJF78JC43dAR385LbrUhnc84,34506 +git/db.py,sha256=Ji8Zrdq5Gvo3Hm54gBB7DXFYJE0SgPmoHyAp_5RI3s0,2255 +git/diff.py,sha256=nzQ2ZtSFrvcpgTvKBKlheLLZGdLgmW3xbwjaZQtaJBQ,22587 +git/exc.py,sha256=rtiF2l2ZONIih_yF3DZ_WuDXAocdq0inhjQzGJq5T7o,6079 +git/index/__init__.py,sha256=43ovvVNocVRNiQd4fLqvUMuGGmwhBQ9SsiQ46vkvk1E,89 +git/index/__pycache__/__init__.cpython-310.pyc,, +git/index/__pycache__/base.cpython-310.pyc,, +git/index/__pycache__/fun.cpython-310.pyc,, +git/index/__pycache__/typ.cpython-310.pyc,, +git/index/__pycache__/util.cpython-310.pyc,, +git/index/base.py,sha256=-MV0W-PyJwCYxFb-Klbooaay3uDm-sr8xHAiK_4lXaU,57001 +git/index/fun.py,sha256=Ac9zLk2JAnayb4Gl9QpJ8S2CzNqWDfxFhZT2S9bhWX8,16431 +git/index/typ.py,sha256=8-yL3QhdHXkVaDHfUuk4Kmks1Comrq547Kg48m6H2gA,5516 +git/index/util.py,sha256=t3llCo90s1L_OgPYIqah5AuVU6043XKTaQrmJtUeYjU,3454 +git/objects/__init__.py,sha256=1uMoWicK_mgiQIaikCMsX7uiRWc9US4XUXCouSmH4Dk,703 +git/objects/__pycache__/__init__.cpython-310.pyc,, +git/objects/__pycache__/base.cpython-310.pyc,, +git/objects/__pycache__/blob.cpython-310.pyc,, +git/objects/__pycache__/commit.cpython-310.pyc,, +git/objects/__pycache__/fun.cpython-310.pyc,, +git/objects/__pycache__/tag.cpython-310.pyc,, +git/objects/__pycache__/tree.cpython-310.pyc,, +git/objects/__pycache__/util.cpython-310.pyc,, +git/objects/base.py,sha256=qLgh-OStkOke3yKgwq2ZNFTC45Qsl1UTSI1-fdjR8-w,7759 +git/objects/blob.py,sha256=nXCRt885vuNjI6VRw_fXOZSgQfD9PjXPg3XZIRZkIfM,987 +git/objects/commit.py,sha256=-dXHQvop5HXIqQjuMYIFMNLL2AY9y0a7Gw3ayG8OAHY,25869 +git/objects/fun.py,sha256=vSmm8p4_6ZMOh3Vtwbi65gP1vIFczXF2hXtgHclP-EY,8542 +git/objects/submodule/__init__.py,sha256=OsMeiex7cG6ev2f35IaJ5csH-eXchSoNKCt4HXUG5Ws,93 +git/objects/submodule/__pycache__/__init__.cpython-310.pyc,, +git/objects/submodule/__pycache__/base.cpython-310.pyc,, +git/objects/submodule/__pycache__/root.cpython-310.pyc,, +git/objects/submodule/__pycache__/util.cpython-310.pyc,, +git/objects/submodule/base.py,sha256=QOZfeU4mQVyGFLKGWS8YTRD8DtN_lVjhtPsw6N9Upqs,58774 +git/objects/submodule/root.py,sha256=cy7wRBLJwqNXGm6bK0tgEucYX7lx-KNQS-WTTlEXvig,18288 +git/objects/submodule/util.py,sha256=iX1EYGDhVrr1PG8729zQPm2GL47FkE9MPqPYC8C_h-o,3358 +git/objects/tag.py,sha256=mqlDG5UyScqHEnwDXRBPwbMcPrTZnhMiezpDM7DkEss,3764 +git/objects/tree.py,sha256=ly7fgePvItjB-B69AvdlFXrekH8MixOMauO31sCpg7E,14292 +git/objects/util.py,sha256=gx1jzp1oiqdlADrWu6oorIF3FwXJZtaprzxPlyY9u8I,22466 +git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +git/refs/__init__.py,sha256=PMF97jMUcivbCCEJnl2zTs-YtECNFp8rL8GHK8AitXU,203 +git/refs/__pycache__/__init__.cpython-310.pyc,, +git/refs/__pycache__/head.cpython-310.pyc,, +git/refs/__pycache__/log.cpython-310.pyc,, +git/refs/__pycache__/reference.cpython-310.pyc,, +git/refs/__pycache__/remote.cpython-310.pyc,, +git/refs/__pycache__/symbolic.cpython-310.pyc,, +git/refs/__pycache__/tag.cpython-310.pyc,, +git/refs/head.py,sha256=UbIuWCb9WI87DZY6puX92YaHLDnh-D6DkONwDjyxcvM,9626 +git/refs/log.py,sha256=8ZTiE7KV4SVAlx14xR7xVG1uh_jFgsSNZ_6QSMh4vpA,12061 +git/refs/reference.py,sha256=28aB_lnYLKIXx2wvy_y87P0EDIuNiCsGLgBmV-29qP0,5405 +git/refs/remote.py,sha256=3iSjMHPlQCUA3QacJ-CHK60yihCmGeyVnuTh-zTq7qo,2556 +git/refs/symbolic.py,sha256=nBmwXAK48w-vmFq-tAl40XlemKsPxiu7aDnyctlWA5o,29739 +git/refs/tag.py,sha256=xboM_oFCFXakpZvvb-bn4GgLcsddPvNIuEK9E3gNuNs,4273 +git/remote.py,sha256=VohJ7s27AC0RCP8EgG-_oMjTE1nxOqnQTHSulhqglnY,41704 +git/repo/__init__.py,sha256=XMpdeowJRtTEd80jAcrKSQfMu2JZGMfPlpuIYHG2ZCk,80 +git/repo/__pycache__/__init__.cpython-310.pyc,, +git/repo/__pycache__/base.cpython-310.pyc,, +git/repo/__pycache__/fun.cpython-310.pyc,, +git/repo/base.py,sha256=mF3lmjtg81KQfst2RZad290fp-bwsEG28dEh7NYkmf0,51847 +git/repo/fun.py,sha256=yjsY_sna6XaUzB0ZANiIMwu97QMxWAWpWtoj4pynqg4,12713 +git/types.py,sha256=L9yBmFn6XUdV9BJgnKSwXPcYEPj9mACl7VuIi7FcNQ8,3072 +git/util.py,sha256=AAebYTIADbA46Iu5rsUqCvCPAM2w-PlbMp2-sm6Wdok,39531 diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/REQUESTED b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/WHEEL b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/WHEEL new file mode 100644 index 0000000..becc9a6 --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/Lib/site-packages/GitPython-3.1.27.dist-info/top_level.txt b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/top_level.txt new file mode 100644 index 0000000..5664e30 --- /dev/null +++ b/venv/Lib/site-packages/GitPython-3.1.27.dist-info/top_level.txt @@ -0,0 +1 @@ +git diff --git a/venv/Lib/site-packages/_distutils_hack/__init__.py b/venv/Lib/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..f987a53 --- /dev/null +++ b/venv/Lib/site-packages/_distutils_hack/__init__.py @@ -0,0 +1,222 @@ +# don't import any costly modules +import sys +import os + + +is_pypy = '__pypy__' in sys.builtin_module_names + + +def warn_distutils_present(): + if 'distutils' not in sys.modules: + return + if is_pypy and sys.version_info < (3, 7): + # PyPy for 3.6 unconditionally imports distutils, so bypass the warning + # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 + return + import warnings + + warnings.warn( + "Distutils was imported before Setuptools, but importing Setuptools " + "also replaces the `distutils` module in `sys.modules`. This may lead " + "to undesirable behaviors or errors. To avoid these issues, avoid " + "using distutils directly, ensure that setuptools is installed in the " + "traditional way (e.g. not an editable install), and/or make sure " + "that setuptools is always imported before distutils." + ) + + +def clear_distutils(): + if 'distutils' not in sys.modules: + return + import warnings + + warnings.warn("Setuptools is replacing distutils.") + mods = [ + name + for name in sys.modules + if name == "distutils" or name.startswith("distutils.") + ] + for name in mods: + del sys.modules[name] + + +def enabled(): + """ + Allow selection of distutils by environment variable. + """ + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local') + return which == 'local' + + +def ensure_local_distutils(): + import importlib + + clear_distutils() + + # With the DistutilsMetaFinder in place, + # perform an import to cause distutils to be + # loaded from setuptools._distutils. Ref #2906. + with shim(): + importlib.import_module('distutils') + + # check that submodules load as expected + core = importlib.import_module('distutils.core') + assert '_distutils' in core.__file__, core.__file__ + assert 'setuptools._distutils.log' not in sys.modules + + +def do_override(): + """ + Ensure that the local copy of distutils is preferred over stdlib. + + See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401 + for more motivation. + """ + if enabled(): + warn_distutils_present() + ensure_local_distutils() + + +class _TrivialRe: + def __init__(self, *patterns): + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + +class DistutilsMetaFinder: + def find_spec(self, fullname, path, target=None): + # optimization: only consider top level modules and those + # found in the CPython test suite. + if path is not None and not fullname.startswith('test.'): + return + + method_name = 'spec_for_{fullname}'.format(**locals()) + method = getattr(self, method_name, lambda: None) + return method() + + def spec_for_distutils(self): + if self.is_cpython(): + return + + import importlib + import importlib.abc + import importlib.util + + try: + mod = importlib.import_module('setuptools._distutils') + except Exception: + # There are a couple of cases where setuptools._distutils + # may not be present: + # - An older Setuptools without a local distutils is + # taking precedence. Ref #2957. + # - Path manipulation during sitecustomize removes + # setuptools from the path but only after the hook + # has been loaded. Ref #2980. + # In either case, fall back to stdlib behavior. + return + + class DistutilsLoader(importlib.abc.Loader): + def create_module(self, spec): + mod.__name__ = 'distutils' + return mod + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader( + 'distutils', DistutilsLoader(), origin=mod.__file__ + ) + + @staticmethod + def is_cpython(): + """ + Suppress supplying distutils for CPython (build and tests). + Ref #2965 and #3007. + """ + return os.path.isfile('pybuilddir.txt') + + def spec_for_pip(self): + """ + Ensure stdlib distutils when running under pip. + See pypa/pip#8761 for rationale. + """ + if self.pip_imported_during_build(): + return + clear_distutils() + self.spec_for_distutils = lambda: None + + @classmethod + def pip_imported_during_build(cls): + """ + Detect if pip is being imported in a build script. Ref #2355. + """ + import traceback + + return any( + cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None) + ) + + @staticmethod + def frame_file_is_setup(frame): + """ + Return True if the indicated frame suggests a setup.py file. + """ + # some frames may not have __file__ (#2940) + return frame.f_globals.get('__file__', '').endswith('setup.py') + + def spec_for_sensitive_tests(self): + """ + Ensure stdlib distutils when running select tests under CPython. + + python/cpython#91169 + """ + clear_distutils() + self.spec_for_distutils = lambda: None + + sensitive_tests = ( + [ + 'test.test_distutils', + 'test.test_peg_generator', + 'test.test_importlib', + ] + if sys.version_info < (3, 10) + else [ + 'test.test_distutils', + ] + ) + + +for name in DistutilsMetaFinder.sensitive_tests: + setattr( + DistutilsMetaFinder, + f'spec_for_{name}', + DistutilsMetaFinder.spec_for_sensitive_tests, + ) + + +DISTUTILS_FINDER = DistutilsMetaFinder() + + +def add_shim(): + DISTUTILS_FINDER in sys.meta_path or insert_shim() + + +class shim: + def __enter__(self): + insert_shim() + + def __exit__(self, exc, value, tb): + remove_shim() + + +def insert_shim(): + sys.meta_path.insert(0, DISTUTILS_FINDER) + + +def remove_shim(): + try: + sys.meta_path.remove(DISTUTILS_FINDER) + except ValueError: + pass diff --git a/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1945fd5 Binary files /dev/null and b/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc b/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc new file mode 100644 index 0000000..e7f802f Binary files /dev/null and b/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/_distutils_hack/override.py b/venv/Lib/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/venv/Lib/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/venv/Lib/site-packages/distutils-precedence.pth b/venv/Lib/site-packages/distutils-precedence.pth new file mode 100644 index 0000000..7f009fe --- /dev/null +++ b/venv/Lib/site-packages/distutils-precedence.pth @@ -0,0 +1 @@ +import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __import__('_distutils_hack').add_shim(); diff --git a/venv/Lib/site-packages/git/__init__.py b/venv/Lib/site-packages/git/__init__.py new file mode 100644 index 0000000..3229b59 --- /dev/null +++ b/venv/Lib/site-packages/git/__init__.py @@ -0,0 +1,90 @@ +# __init__.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +# flake8: noqa +#@PydevCodeAnalysisIgnore +from git.exc import * # @NoMove @IgnorePep8 +import inspect +import os +import sys +import os.path as osp + +from typing import Optional +from git.types import PathLike + +__version__ = '3.1.27' + + +#{ Initialization +def _init_externals() -> None: + """Initialize external projects by putting them into the path""" + if __version__ == '3.1.27' and 'PYOXIDIZER' not in os.environ: + sys.path.insert(1, osp.join(osp.dirname(__file__), 'ext', 'gitdb')) + + try: + import gitdb + except ImportError as e: + raise ImportError("'gitdb' could not be found in your PYTHONPATH") from e + # END verify import + +#} END initialization + + +################# +_init_externals() +################# + +#{ Imports + +try: + from git.config import GitConfigParser # @NoMove @IgnorePep8 + from git.objects import * # @NoMove @IgnorePep8 + from git.refs import * # @NoMove @IgnorePep8 + from git.diff import * # @NoMove @IgnorePep8 + from git.db import * # @NoMove @IgnorePep8 + from git.cmd import Git # @NoMove @IgnorePep8 + from git.repo import Repo # @NoMove @IgnorePep8 + from git.remote import * # @NoMove @IgnorePep8 + from git.index import * # @NoMove @IgnorePep8 + from git.util import ( # @NoMove @IgnorePep8 + LockFile, + BlockingLockFile, + Stats, + Actor, + rmtree, + ) +except GitError as exc: + raise ImportError('%s: %s' % (exc.__class__.__name__, exc)) from exc + +#} END imports + +__all__ = [name for name, obj in locals().items() + if not (name.startswith('_') or inspect.ismodule(obj))] + + +#{ Initialize git executable path +GIT_OK = None + + +def refresh(path: Optional[PathLike] = None) -> None: + """Convenience method for setting the git executable path.""" + global GIT_OK + GIT_OK = False + + if not Git.refresh(path=path): + return + if not FetchInfo.refresh(): + return + + GIT_OK = True +#} END initialize git executable path + + +################# +try: + refresh() +except Exception as exc: + raise ImportError('Failed to initialize: {0}'.format(exc)) from exc +################# diff --git a/venv/Lib/site-packages/git/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..0793eb0 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/cmd.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/cmd.cpython-310.pyc new file mode 100644 index 0000000..6608548 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/cmd.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/compat.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/compat.cpython-310.pyc new file mode 100644 index 0000000..a5fbdb4 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/compat.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/config.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..3e7c44a Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/config.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/db.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/db.cpython-310.pyc new file mode 100644 index 0000000..c8c2a4b Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/db.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/diff.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/diff.cpython-310.pyc new file mode 100644 index 0000000..e16ca65 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/diff.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/exc.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/exc.cpython-310.pyc new file mode 100644 index 0000000..926439e Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/exc.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/remote.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/remote.cpython-310.pyc new file mode 100644 index 0000000..2135be1 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/remote.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/types.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/types.cpython-310.pyc new file mode 100644 index 0000000..9fb7dc5 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/types.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/__pycache__/util.cpython-310.pyc b/venv/Lib/site-packages/git/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000..d8645f6 Binary files /dev/null and b/venv/Lib/site-packages/git/__pycache__/util.cpython-310.pyc differ diff --git a/venv/Lib/site-packages/git/cmd.py b/venv/Lib/site-packages/git/cmd.py new file mode 100644 index 0000000..4f05698 --- /dev/null +++ b/venv/Lib/site-packages/git/cmd.py @@ -0,0 +1,1286 @@ +# cmd.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +from __future__ import annotations +from contextlib import contextmanager +import io +import logging +import os +import signal +from subprocess import ( + call, + Popen, + PIPE, + DEVNULL +) +import subprocess +import threading +from textwrap import dedent + +from git.compat import ( + defenc, + force_bytes, + safe_decode, + is_posix, + is_win, +) +from git.exc import CommandError +from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present + +from .exc import ( + GitCommandError, + GitCommandNotFound +) +from .util import ( + LazyMixin, + stream_copy, +) + +# typing --------------------------------------------------------------------------- + +from typing import (Any, AnyStr, BinaryIO, Callable, Dict, IO, Iterator, List, Mapping, + Sequence, TYPE_CHECKING, TextIO, Tuple, Union, cast, overload) + +from git.types import PathLike, Literal, TBD + +if TYPE_CHECKING: + from git.repo.base import Repo + from git.diff import DiffIndex + + +# --------------------------------------------------------------------------------- + +execute_kwargs = {'istream', 'with_extended_output', + 'with_exceptions', 'as_process', 'stdout_as_string', + 'output_stream', 'with_stdout', 'kill_after_timeout', + 'universal_newlines', 'shell', 'env', 'max_chunk_size'} + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) + +__all__ = ('Git',) + + +# ============================================================================== +## @name Utilities +# ------------------------------------------------------------------------------ +# Documentation +## @{ + +def handle_process_output(process: 'Git.AutoInterrupt' | Popen, + stdout_handler: Union[None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None], + Callable[[bytes, 'Repo', 'DiffIndex'], None]], + stderr_handler: Union[None, + Callable[[AnyStr], None], + Callable[[List[AnyStr]], None]], + finalizer: Union[None, + Callable[[Union[subprocess.Popen, 'Git.AutoInterrupt']], None]] = None, + decode_streams: bool = True, + kill_after_timeout: Union[None, float] = None) -> None: + """Registers for notifications to learn that process output is ready to read, and dispatches lines to + the respective line handlers. + This function returns once the finalizer returns + + :return: result of finalizer + :param process: subprocess.Popen instance + :param stdout_handler: f(stdout_line_string), or None + :param stderr_handler: f(stderr_line_string), or None + :param finalizer: f(proc) - wait for proc to finish + :param decode_streams: + Assume stdout/stderr streams are binary and decode them before pushing \ + their contents to handlers. + Set it to False if `universal_newline == True` (then streams are in text-mode) + or if decoding must happen later (i.e. for Diffs). + :param kill_after_timeout: + float or None, Default = None + To specify a timeout in seconds for the git command, after which the process + should be killed. + """ + # Use 2 "pump" threads and wait for both to finish. + def pump_stream(cmdline: List[str], name: str, stream: Union[BinaryIO, TextIO], is_decode: bool, + handler: Union[None, Callable[[Union[bytes, str]], None]]) -> None: + try: + for line in stream: + if handler: + if is_decode: + assert isinstance(line, bytes) + line_str = line.decode(defenc) + handler(line_str) + else: + handler(line) + + except Exception as ex: + log.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}") + if "I/O operation on closed file" not in str(ex): + # Only reraise if the error was not due to the stream closing + raise CommandError([f'<{name}-pump>'] + remove_password_if_present(cmdline), ex) from ex + finally: + stream.close() + + if hasattr(process, 'proc'): + process = cast('Git.AutoInterrupt', process) + cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, 'args', '') + p_stdout = process.proc.stdout if process.proc else None + p_stderr = process.proc.stderr if process.proc else None + else: + process = cast(Popen, process) + cmdline = getattr(process, 'args', '') + p_stdout = process.stdout + p_stderr = process.stderr + + if not isinstance(cmdline, (tuple, list)): + cmdline = cmdline.split() + + pumps: List[Tuple[str, IO, Callable[..., None] | None]] = [] + if p_stdout: + pumps.append(('stdout', p_stdout, stdout_handler)) + if p_stderr: + pumps.append(('stderr', p_stderr, stderr_handler)) + + threads: List[threading.Thread] = [] + + for name, stream, handler in pumps: + t = threading.Thread(target=pump_stream, + args=(cmdline, name, stream, decode_streams, handler)) + t.daemon = True + t.start() + threads.append(t) + + ## FIXME: Why Join?? Will block if `stdin` needs feeding... + # + for t in threads: + t.join(timeout=kill_after_timeout) + if t.is_alive(): + if isinstance(process, Git.AutoInterrupt): + process._terminate() + else: # Don't want to deal with the other case + raise RuntimeError("Thread join() timed out in cmd.handle_process_output()." + f" kill_after_timeout={kill_after_timeout} seconds") + if stderr_handler: + error_str: Union[str, bytes] = ( + "error: process killed because it timed out." + f" kill_after_timeout={kill_after_timeout} seconds") + if not decode_streams and isinstance(p_stderr, BinaryIO): + # Assume stderr_handler needs binary input + error_str = cast(str, error_str) + error_str = error_str.encode() + # We ignore typing on the next line because mypy does not like + # the way we inferred that stderr takes str or bytes + stderr_handler(error_str) # type: ignore + + if finalizer: + return finalizer(process) + else: + return None + + +def dashify(string: str) -> str: + return string.replace('_', '-') + + +def slots_to_dict(self: object, exclude: Sequence[str] = ()) -> Dict[str, Any]: + return {s: getattr(self, s) for s in self.__slots__ if s not in exclude} + + +def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], excluded: Sequence[str] = ()) -> None: + for k, v in d.items(): + setattr(self, k, v) + for k in excluded: + setattr(self, k, None) + +## -- End Utilities -- @} + + +# value of Windows process creation flag taken from MSDN +CREATE_NO_WINDOW = 0x08000000 + +## CREATE_NEW_PROCESS_GROUP is needed to allow killing it afterwards, +# see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal +PROC_CREATIONFLAGS = (CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] + if is_win else 0) # mypy error if not windows + + +class Git(LazyMixin): + + """ + The Git class manages communication with the Git binary. + + It provides a convenient interface to calling the Git binary, such as in:: + + g = Git( git_dir ) + g.init() # calls 'git init' program + rval = g.ls_files() # calls 'git ls-files' program + + ``Debugging`` + Set the GIT_PYTHON_TRACE environment variable print each invocation + of the command to stdout. + Set its value to 'full' to see details about the returned values. + """ + __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info", + "_git_options", "_persistent_git_options", "_environment") + + _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info') + + def __getstate__(self) -> Dict[str, Any]: + return slots_to_dict(self, exclude=self._excluded_) + + def __setstate__(self, d: Dict[str, Any]) -> None: + dict_to_slots_and__excluded_are_none(self, d, excluded=self._excluded_) + + # CONFIGURATION + + git_exec_name = "git" # default that should work on linux and windows + + # Enables debugging of GitPython's git commands + GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) + + # If True, a shell will be used when executing git commands. + # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126 + # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required. + # Override this value using `Git.USE_SHELL = True` + USE_SHELL = False + + # Provide the full path to the git executable. Otherwise it assumes git is in the path + _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" + _refresh_env_var = "GIT_PYTHON_REFRESH" + GIT_PYTHON_GIT_EXECUTABLE = None + # note that the git executable is actually found during the refresh step in + # the top level __init__ + + @classmethod + def refresh(cls, path: Union[None, PathLike] = None) -> bool: + """This gets called by the refresh function (see the top level + __init__). + """ + # discern which path to refresh with + if path is not None: + new_git = os.path.expanduser(path) + new_git = os.path.abspath(new_git) + else: + new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name) + + # keep track of the old and new git executable path + old_git = cls.GIT_PYTHON_GIT_EXECUTABLE + cls.GIT_PYTHON_GIT_EXECUTABLE = new_git + + # test if the new git executable path is valid + + # - a GitCommandNotFound error is spawned by ourselves + # - a PermissionError is spawned if the git executable provided + # cannot be executed for whatever reason + + has_git = False + try: + cls().version() + has_git = True + except (GitCommandNotFound, PermissionError): + pass + + # warn or raise exception if test failed + if not has_git: + err = dedent("""\ + Bad git executable. + The git executable must be specified in one of the following ways: + - be included in your $PATH + - be set via $%s + - explicitly set via git.refresh() + """) % cls._git_exec_env_var + + # revert to whatever the old_git was + cls.GIT_PYTHON_GIT_EXECUTABLE = old_git + + if old_git is None: + # on the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is + # None) we only are quiet, warn, or error depending on the + # GIT_PYTHON_REFRESH value + + # determine what the user wants to happen during the initial + # refresh we expect GIT_PYTHON_REFRESH to either be unset or + # be one of the following values: + # 0|q|quiet|s|silence + # 1|w|warn|warning + # 2|r|raise|e|error + + mode = os.environ.get(cls._refresh_env_var, "raise").lower() + + quiet = ["quiet", "q", "silence", "s", "none", "n", "0"] + warn = ["warn", "w", "warning", "1"] + error = ["error", "e", "raise", "r", "2"] + + if mode in quiet: + pass + elif mode in warn or mode in error: + err = dedent("""\ + %s + All git commands will error until this is rectified. + + This initial warning can be silenced or aggravated in the future by setting the + $%s environment variable. Use one of the following values: + - %s: for no warning or exception + - %s: for a printed warning + - %s: for a raised exception + + Example: + export %s=%s + """) % ( + err, + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + cls._refresh_env_var, + quiet[0]) + + if mode in warn: + print("WARNING: %s" % err) + else: + raise ImportError(err) + else: + err = dedent("""\ + %s environment variable has been set but it has been set with an invalid value. + + Use only the following values: + - %s: for no warning or exception + - %s: for a printed warning + - %s: for a raised exception + """) % ( + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error)) + raise ImportError(err) + + # we get here if this was the init refresh and the refresh mode + # was not error, go ahead and set the GIT_PYTHON_GIT_EXECUTABLE + # such that we discern the difference between a first import + # and a second import + cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name + else: + # after the first refresh (when GIT_PYTHON_GIT_EXECUTABLE + # is no longer None) we raise an exception + raise GitCommandNotFound("git", err) + + return has_git + + @classmethod + def is_cygwin(cls) -> bool: + return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE) + + @overload + @classmethod + def polish_url(cls, url: str, is_cygwin: Literal[False] = ...) -> str: + ... + + @overload + @classmethod + def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: + ... + + @classmethod + def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: + if is_cygwin is None: + is_cygwin = cls.is_cygwin() + + if is_cygwin: + url = cygpath(url) + else: + """Remove any backslahes from urls to be written in config files. + + Windows might create config-files containing paths with backslashed, + but git stops liking them as it will escape the backslashes. + Hence we undo the escaping just to be sure. + """ + url = os.path.expandvars(url) + if url.startswith('~'): + url = os.path.expanduser(url) + url = url.replace("\\\\", "\\").replace("\\", "/") + return url + + class AutoInterrupt(object): + """Kill/Interrupt the stored process instance once this instance goes out of scope. It is + used to prevent processes piling up in case iterators stop reading. + Besides all attributes are wired through to the contained process object. + + The wait method was overridden to perform automatic status code checking + and possibly raise.""" + + __slots__ = ("proc", "args", "status") + + # If this is non-zero it will override any status code during + # _terminate, used to prevent race conditions in testing + _status_code_if_terminate: int = 0 + + def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: + self.proc = proc + self.args = args + self.status: Union[int, None] = None + + def _terminate(self) -> None: + """Terminate the underlying process""" + if self.proc is None: + return + + proc = self.proc + self.proc = None + if proc.stdin: + proc.stdin.close() + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() + # did the process finish already so we have a return code ? + try: + if proc.poll() is not None: + self.status = self._status_code_if_terminate or proc.poll() + return None + except OSError as ex: + log.info("Ignored error after process had died: %r", ex) + + # can be that nothing really exists anymore ... + if os is None or getattr(os, 'kill', None) is None: + return None + + # try to kill it + try: + proc.terminate() + status = proc.wait() # ensure process goes away + + self.status = self._status_code_if_terminate or status + except OSError as ex: + log.info("Ignored error after process had died: %r", ex) + except AttributeError: + # try windows + # for some reason, providing None for stdout/stderr still prints something. This is why + # we simply use the shell and redirect to nul. Its slower than CreateProcess, question + # is whether we really want to see all these messages. Its annoying no matter what. + if is_win: + call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), shell=True) + # END exception handling + + def __del__(self) -> None: + self._terminate() + + def __getattr__(self, attr: str) -> Any: + return getattr(self.proc, attr) + + # TODO: Bad choice to mimic `proc.wait()` but with different args. + def wait(self, stderr: Union[None, str, bytes] = b'') -> int: + """Wait for the process and return its status code. + + :param stderr: Previously read value of stderr, in case stderr is already closed. + :warn: may deadlock if output or error pipes are used and not handled separately. + :raise GitCommandError: if the return status is not 0""" + if stderr is None: + stderr_b = b'' + stderr_b = force_bytes(data=stderr, encoding='utf-8') + status: Union[int, None] + if self.proc is not None: + status = self.proc.wait() + p_stderr = self.proc.stderr + else: # Assume the underlying proc was killed earlier or never existed + status = self.status + p_stderr = None + + def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: + if stream: + try: + return stderr_b + force_bytes(stream.read()) + except ValueError: + return stderr_b or b'' + else: + return stderr_b or b'' + + # END status handling + + if status != 0: + errstr = read_all_from_possibly_closed_stream(p_stderr) + log.debug('AutoInterrupt wait stderr: %r' % (errstr,)) + raise GitCommandError(remove_password_if_present(self.args), status, errstr) + return status + + # END auto interrupt + + class CatFileContentStream(object): + + """Object representing a sized read-only stream returning the contents of + an object. + It behaves like a stream, but counts the data read and simulates an empty + stream once our sized content region is empty. + If not all data is read to the end of the objects's lifetime, we read the + rest to assure the underlying stream continues to work""" + + __slots__: Tuple[str, ...] = ('_stream', '_nbr', '_size') + + def __init__(self, size: int, stream: IO[bytes]) -> None: + self._stream = stream + self._size = size + self._nbr = 0 # num bytes read + + # special case: if the object is empty, has null bytes, get the + # final newline right away. + if size == 0: + stream.read(1) + # END handle empty streams + + def read(self, size: int = -1) -> bytes: + bytes_left = self._size - self._nbr + if bytes_left == 0: + return b'' + if size > -1: + # assure we don't try to read past our limit + size = min(bytes_left, size) + else: + # they try to read all, make sure its not more than what remains + size = bytes_left + # END check early depletion + data = self._stream.read(size) + self._nbr += len(data) + + # check for depletion, read our final byte to make the stream usable by others + if self._size - self._nbr == 0: + self._stream.read(1) # final newline + # END finish reading + return data + + def readline(self, size: int = -1) -> bytes: + if self._nbr == self._size: + return b'' + + # clamp size to lowest allowed value + bytes_left = self._size - self._nbr + if size > -1: + size = min(bytes_left, size) + else: + size = bytes_left + # END handle size + + data = self._stream.readline(size) + self._nbr += len(data) + + # handle final byte + if self._size - self._nbr == 0: + self._stream.read(1) + # END finish reading + + return data + + def readlines(self, size: int = -1) -> List[bytes]: + if self._nbr == self._size: + return [] + + # leave all additional logic to our readline method, we just check the size + out = [] + nbr = 0 + while True: + line = self.readline() + if not line: + break + out.append(line) + if size > -1: + nbr += len(line) + if nbr > size: + break + # END handle size constraint + # END readline loop + return out + + # skipcq: PYL-E0301 + def __iter__(self) -> 'Git.CatFileContentStream': + return self + + def __next__(self) -> bytes: + return next(self) + + def next(self) -> bytes: + line = self.readline() + if not line: + raise StopIteration + + return line + + def __del__(self) -> None: + bytes_left = self._size - self._nbr + if bytes_left: + # read and discard - seeking is impossible within a stream + # includes terminating newline + self._stream.read(bytes_left + 1) + # END handle incomplete read + + def __init__(self, working_dir: Union[None, PathLike] = None): + """Initialize this instance with: + + :param working_dir: + Git directory we should work in. If None, we always work in the current + directory as returned by os.getcwd(). + It is meant to be the working tree directory if available, or the + .git directory in case of bare repositories.""" + super(Git, self).__init__() + self._working_dir = expand_path(working_dir) + self._git_options: Union[List[str], Tuple[str, ...]] = () + self._persistent_git_options: List[str] = [] + + # Extra environment variables to pass to git commands + self._environment: Dict[str, str] = {} + + # cached command slots + self.cat_file_header: Union[None, TBD] = None + self.cat_file_all: Union[None, TBD] = None + + def __getattr__(self, name: str) -> Any: + """A convenience method as it allows to call the command as if it was + an object. + :return: Callable object that will execute call _call_process with your arguments.""" + if name[0] == '_': + return LazyMixin.__getattr__(self, name) + return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) + + def set_persistent_git_options(self, **kwargs: Any) -> None: + """Specify command line options to the git executable + for subsequent subcommand calls + + :param kwargs: + is a dict of keyword arguments. + these arguments are passed as in _call_process + but will be passed to the git command rather than + the subcommand. + """ + + self._persistent_git_options = self.transform_kwargs( + split_single_char_options=True, **kwargs) + + def _set_cache_(self, attr: str) -> None: + if attr == '_version_info': + # We only use the first 4 numbers, as everything else could be strings in fact (on windows) + process_version = self._call_process('version') # should be as default *args and **kwargs used + version_numbers = process_version.split(' ')[2] + + self._version_info = cast(Tuple[int, int, int, int], + tuple(int(n) for n in version_numbers.split('.')[:4] if n.isdigit()) + ) + else: + super(Git, self)._set_cache_(attr) + # END handle version info + + @ property + def working_dir(self) -> Union[None, PathLike]: + """:return: Git directory we are working on""" + return self._working_dir + + @ property + def version_info(self) -> Tuple[int, int, int, int]: + """ + :return: tuple(int, int, int, int) tuple with integers representing the major, minor + and additional version numbers as parsed from git version. + This value is generated on demand and is cached""" + return self._version_info + + @ overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[True] + ) -> 'AutoInterrupt': + ... + + @ overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[True] + ) -> Union[str, Tuple[int, str, str]]: + ... + + @ overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + as_process: Literal[False] = False, + stdout_as_string: Literal[False] = False + ) -> Union[bytes, Tuple[int, bytes, str]]: + ... + + @ overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[True] + ) -> str: + ... + + @ overload + def execute(self, + command: Union[str, Sequence[Any]], + *, + with_extended_output: Literal[False], + as_process: Literal[False], + stdout_as_string: Literal[False] + ) -> bytes: + ... + + def execute(self, + command: Union[str, Sequence[Any]], + istream: Union[None, BinaryIO] = None, + with_extended_output: bool = False, + with_exceptions: bool = True, + as_process: bool = False, + output_stream: Union[None, BinaryIO] = None, + stdout_as_string: bool = True, + kill_after_timeout: Union[None, float] = None, + with_stdout: bool = True, + universal_newlines: bool = False, + shell: Union[None, bool] = None, + env: Union[None, Mapping[str, str]] = None, + max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, + **subprocess_kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: + """Handles executing the command on the shell and consumes and returns + the returned information (stdout) + + :param command: + The command argument list to execute. + It should be a string, or a sequence of program arguments. The + program to execute is the first item in the args sequence or string. + + :param istream: + Standard input filehandle passed to subprocess.Popen. + + :param with_extended_output: + Whether to return a (status, stdout, stderr) tuple. + + :param with_exceptions: + Whether to raise an exception when git returns a non-zero status. + + :param as_process: + Whether to return the created process instance directly from which + streams can be read on demand. This will render with_extended_output and + with_exceptions ineffective - the caller will have + to deal with the details himself. + It is important to note that the process will be placed into an AutoInterrupt + wrapper that will interrupt the process once it goes out of scope. If you + use the command in iterators, you should pass the whole process instance + instead of a single stream. + + :param output_stream: + If set to a file-like object, data produced by the git command will be + output to the given stream directly. + This feature only has any effect if as_process is False. Processes will + always be created with a pipe due to issues with subprocess. + This merely is a workaround as data will be copied from the + output pipe to the given output stream directly. + Judging from the implementation, you shouldn't use this flag ! + + :param stdout_as_string: + if False, the commands standard output will be bytes. Otherwise, it will be + decoded into a string using the default encoding (usually utf-8). + The latter can fail, if the output contains binary data. + + :param env: + A dictionary of environment variables to be passed to `subprocess.Popen`. + + :param max_chunk_size: + Maximum number of bytes in one chunk of data passed to the output_stream in + one invocation of write() method. If the given number is not positive then + the default value is used. + + :param subprocess_kwargs: + Keyword arguments to be passed to subprocess.Popen. Please note that + some of the valid kwargs are already set by this method, the ones you + specify may not be the same ones. + + :param with_stdout: If True, default True, we open stdout on the created process + :param universal_newlines: + if True, pipes will be opened as text, and lines are split at + all known line endings. + :param shell: + Whether to invoke commands through a shell (see `Popen(..., shell=True)`). + It overrides :attr:`USE_SHELL` if it is not `None`. + :param kill_after_timeout: + To specify a timeout in seconds for the git command, after which the process + should be killed. This will have no effect if as_process is set to True. It is + set to None by default and will let the process run until the timeout is + explicitly specified. This feature is not supported on Windows. It's also worth + noting that kill_after_timeout uses SIGKILL, which can have negative side + effects on a repository. For example, stale locks in case of git gc could + render the repository incapable of accepting changes until the lock is manually + removed. + + :return: + * str(output) if extended_output = False (Default) + * tuple(int(status), str(stdout), str(stderr)) if extended_output = True + + if output_stream is True, the stdout value will be your output stream: + * output_stream if extended_output = False + * tuple(int(status), output_stream, str(stderr)) if extended_output = True + + Note git is executed with LC_MESSAGES="C" to ensure consistent + output regardless of system language. + + :raise GitCommandError: + + :note: + If you add additional keyword arguments to the signature of this method, + you must update the execute_kwargs tuple housed in this module.""" + # Remove password for the command if present + redacted_command = remove_password_if_present(command) + if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != 'full' or as_process): + log.info(' '.join(redacted_command)) + + # Allow the user to have the command executed in their working dir. + try: + cwd = self._working_dir or os.getcwd() # type: Union[None, str] + if not os.access(str(cwd), os.X_OK): + cwd = None + except FileNotFoundError: + cwd = None + + # Start the process + inline_env = env + env = os.environ.copy() + # Attempt to force all output to plain ascii english, which is what some parsing code + # may expect. + # According to stackoverflow (http://goo.gl/l74GC8), we are setting LANGUAGE as well + # just to be sure. + env["LANGUAGE"] = "C" + env["LC_ALL"] = "C" + env.update(self._environment) + if inline_env is not None: + env.update(inline_env) + + if is_win: + cmd_not_found_exception = OSError + if kill_after_timeout is not None: + raise GitCommandError(redacted_command, '"kill_after_timeout" feature is not supported on Windows.') + else: + cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable + # end handle + + stdout_sink = (PIPE + if with_stdout + else getattr(subprocess, 'DEVNULL', None) or open(os.devnull, 'wb')) + istream_ok = "None" + if istream: + istream_ok = "" + log.debug("Popen(%s, cwd=%s, universal_newlines=%s, shell=%s, istream=%s)", + redacted_command, cwd, universal_newlines, shell, istream_ok) + try: + proc = Popen(command, + env=env, + cwd=cwd, + bufsize=-1, + stdin=istream or DEVNULL, + stderr=PIPE, + stdout=stdout_sink, + shell=shell is not None and shell or self.USE_SHELL, + close_fds=is_posix, # unsupported on windows + universal_newlines=universal_newlines, + creationflags=PROC_CREATIONFLAGS, + **subprocess_kwargs + ) + + except cmd_not_found_exception as err: + raise GitCommandNotFound(redacted_command, err) from err + else: + # replace with a typeguard for Popen[bytes]? + proc.stdout = cast(BinaryIO, proc.stdout) + proc.stderr = cast(BinaryIO, proc.stderr) + + if as_process: + return self.AutoInterrupt(proc, command) + + def _kill_process(pid: int) -> None: + """ Callback method to kill a process. """ + p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE, + creationflags=PROC_CREATIONFLAGS) + child_pids = [] + if p.stdout is not None: + for line in p.stdout: + if len(line.split()) > 0: + local_pid = (line.split())[0] + if local_pid.isdigit(): + child_pids.append(int(local_pid)) + try: + # Windows does not have SIGKILL, so use SIGTERM instead + sig = getattr(signal, 'SIGKILL', signal.SIGTERM) + os.kill(pid, sig) + for child_pid in child_pids: + try: + os.kill(child_pid, sig) + except OSError: + pass + kill_check.set() # tell the main routine that the process was killed + except OSError: + # It is possible that the process gets completed in the duration after timeout + # happens and before we try to kill the process. + pass + return + # end + + if kill_after_timeout is not None: + kill_check = threading.Event() + watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid,)) + + # Wait for the process to return + status = 0 + stdout_value: Union[str, bytes] = b'' + stderr_value: Union[str, bytes] = b'' + newline = "\n" if universal_newlines else b"\n" + try: + if output_stream is None: + if kill_after_timeout is not None: + watchdog.start() + stdout_value, stderr_value = proc.communicate() + if kill_after_timeout is not None: + watchdog.cancel() + if kill_check.is_set(): + stderr_value = ('Timeout: the command "%s" did not complete in %d ' + 'secs.' % (" ".join(redacted_command), kill_after_timeout)) + if not universal_newlines: + stderr_value = stderr_value.encode(defenc) + # strip trailing "\n" + if stdout_value.endswith(newline): # type: ignore + stdout_value = stdout_value[:-1] + if stderr_value.endswith(newline): # type: ignore + stderr_value = stderr_value[:-1] + + status = proc.returncode + else: + max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE + stream_copy(proc.stdout, output_stream, max_chunk_size) + stdout_value = proc.stdout.read() + stderr_value = proc.stderr.read() + # strip trailing "\n" + if stderr_value.endswith(newline): # type: ignore + stderr_value = stderr_value[:-1] + status = proc.wait() + # END stdout handling + finally: + proc.stdout.close() + proc.stderr.close() + + if self.GIT_PYTHON_TRACE == 'full': + cmdstr = " ".join(redacted_command) + + def as_text(stdout_value: Union[bytes, str]) -> str: + return not output_stream and safe_decode(stdout_value) or '' + # end + + if stderr_value: + log.info("%s -> %d; stdout: '%s'; stderr: '%s'", + cmdstr, status, as_text(stdout_value), safe_decode(stderr_value)) + elif stdout_value: + log.info("%s -> %d; stdout: '%s'", cmdstr, status, as_text(stdout_value)) + else: + log.info("%s -> %d", cmdstr, status) + # END handle debug printing + + if with_exceptions and status != 0: + raise GitCommandError(redacted_command, status, stderr_value, stdout_value) + + if isinstance(stdout_value, bytes) and stdout_as_string: # could also be output_stream + stdout_value = safe_decode(stdout_value) + + # Allow access to the command's status code + if with_extended_output: + return (status, stdout_value, safe_decode(stderr_value)) + else: + return stdout_value + + def environment(self) -> Dict[str, str]: + return self._environment + + def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: + """ + Set environment variables for future git invocations. Return all changed + values in a format that can be passed back into this function to revert + the changes: + + ``Examples``:: + + old_env = self.update_environment(PWD='/tmp') + self.update_environment(**old_env) + + :param kwargs: environment variables to use for git processes + :return: dict that maps environment variables to their old values + """ + old_env = {} + for key, value in kwargs.items(): + # set value if it is None + if value is not None: + old_env[key] = self._environment.get(key) + self._environment[key] = value + # remove key from environment if its value is None + elif key in self._environment: + old_env[key] = self._environment[key] + del self._environment[key] + return old_env + + @contextmanager + def custom_environment(self, **kwargs: Any) -> Iterator[None]: + """ + A context manager around the above ``update_environment`` method to restore the + environment back to its previous state after operation. + + ``Examples``:: + + with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'): + repo.remotes.origin.fetch() + + :param kwargs: see update_environment + """ + old_env = self.update_environment(**kwargs) + try: + yield + finally: + self.update_environment(**old_env) + + def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool) -> List[str]: + if len(name) == 1: + if value is True: + return ["-%s" % name] + elif value not in (False, None): + if split_single_char_options: + return ["-%s" % name, "%s" % value] + else: + return ["-%s%s" % (name, value)] + else: + if value is True: + return ["--%s" % dashify(name)] + elif value is not False and value is not None: + return ["--%s=%s" % (dashify(name), value)] + return [] + + def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: + """Transforms Python style kwargs into git command line options.""" + args = [] + for k, v in kwargs.items(): + if isinstance(v, (list, tuple)): + for value in v: + args += self.transform_kwarg(k, value, split_single_char_options) + else: + args += self.transform_kwarg(k, v, split_single_char_options) + return args + + @classmethod + def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]: + + outlist = [] + if isinstance(arg_list, (list, tuple)): + for arg in arg_list: + outlist.extend(cls.__unpack_args(arg)) + else: + outlist.append(str(arg_list)) + + return outlist + + def __call__(self, **kwargs: Any) -> 'Git': + """Specify command line options to the git executable + for a subcommand call + + :param kwargs: + is a dict of keyword arguments. + these arguments are passed as in _call_process + but will be passed to the git command rather than + the subcommand. + + ``Examples``:: + git(work_tree='/tmp').difftool()""" + self._git_options = self.transform_kwargs( + split_single_char_options=True, **kwargs) + return self + + @overload + def _call_process(self, method: str, *args: None, **kwargs: None + ) -> str: + ... # if no args given, execute called with all defaults + + @overload + def _call_process(self, method: str, + istream: int, + as_process: Literal[True], + *args: Any, **kwargs: Any + ) -> 'Git.AutoInterrupt': ... + + @overload + def _call_process(self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + ... + + def _call_process(self, method: str, *args: Any, **kwargs: Any + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], 'Git.AutoInterrupt']: + """Run the given git command with the specified arguments and return + the result as a String + + :param method: + is the command. Contained "_" characters will be converted to dashes, + such as in 'ls_files' to call 'ls-files'. + + :param args: + is the list of arguments. If None is included, it will be pruned. + This allows your commands to call git more conveniently as None + is realized as non-existent + + :param kwargs: + It contains key-values for the following: + - the :meth:`execute()` kwds, as listed in :var:`execute_kwargs`; + - "command options" to be converted by :meth:`transform_kwargs()`; + - the `'insert_kwargs_after'` key which its value must match one of ``*args`` + and any cmd-options will be appended after the matched arg. + + Examples:: + + git.rev_list('master', max_count=10, header=True) + + turns into:: + + git rev-list max-count 10 --header master + + :return: Same as ``execute`` + if no args given used execute default (esp. as_process = False, stdout_as_string = True) + and return str """ + # Handle optional arguments prior to calling transform_kwargs + # otherwise these'll end up in args, which is bad. + exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} + opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs} + + insert_after_this_arg = opts_kwargs.pop('insert_kwargs_after', None) + + # Prepare the argument list + + opt_args = self.transform_kwargs(**opts_kwargs) + ext_args = self.__unpack_args([a for a in args if a is not None]) + + if insert_after_this_arg is None: + args_list = opt_args + ext_args + else: + try: + index = ext_args.index(insert_after_this_arg) + except ValueError as err: + raise ValueError("Couldn't find argument '%s' in args %s to insert cmd options after" + % (insert_after_this_arg, str(ext_args))) from err + # end handle error + args_list = ext_args[:index + 1] + opt_args + ext_args[index + 1:] + # end handle opts_kwargs + + call = [self.GIT_PYTHON_GIT_EXECUTABLE] + + # add persistent git options + call.extend(self._persistent_git_options) + + # add the git options, then reset to empty + # to avoid side_effects + call.extend(self._git_options) + self._git_options = () + + call.append(dashify(method)) + call.extend(args_list) + + return self.execute(call, **exec_kwargs) + + def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: + """ + :param header_line: + type_string size_as_int + + :return: (hex_sha, type_string, size_as_int) + + :raise ValueError: if the header contains indication for an error due to + incorrect input sha""" + tokens = header_line.split() + if len(tokens) != 3: + if not tokens: + raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip())) + else: + raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) + # END handle actual return value + # END error handling + + if len(tokens[0]) != 40: + raise ValueError("Failed to parse header: %r" % header_line) + return (tokens[0], tokens[1], int(tokens[2])) + + def _prepare_ref(self, ref: AnyStr) -> bytes: + # required for command to separate refs on stdin, as bytes + if isinstance(ref, bytes): + # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text + refstr: str = ref.decode('ascii') + elif not isinstance(ref, str): + refstr = str(ref) # could be ref-object + else: + refstr = ref + + if not refstr.endswith("\n"): + refstr += "\n" + return refstr.encode(defenc) + + def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwargs: Any + ) -> 'Git.AutoInterrupt': + cur_val = getattr(self, attr_name) + if cur_val is not None: + return cur_val + + options = {"istream": PIPE, "as_process": True} + options.update(kwargs) + + cmd = self._call_process(cmd_name, *args, **options) + setattr(self, attr_name, cmd) + cmd = cast('Git.AutoInterrupt', cmd) + return cmd + + def __get_object_header(self, cmd: 'Git.AutoInterrupt', ref: AnyStr) -> Tuple[str, str, int]: + if cmd.stdin and cmd.stdout: + cmd.stdin.write(self._prepare_ref(ref)) + cmd.stdin.flush() + return self._parse_object_header(cmd.stdout.readline()) + else: + raise ValueError("cmd stdin was empty") + + def get_object_header(self, ref: str) -> Tuple[str, str, int]: + """ Use this method to quickly examine the type and size of the object behind + the given ref. + + :note: The method will only suffer from the costs of command invocation + once and reuses the command in subsequent calls. + + :return: (hexsha, type_string, size_as_int)""" + cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) + return self.__get_object_header(cmd, ref) + + def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: + """ As get_object_header, but returns object data as well + :return: (hexsha, type_string, size_as_int,data_string) + :note: not threadsafe""" + hexsha, typename, size, stream = self.stream_object_data(ref) + data = stream.read(size) + del(stream) + return (hexsha, typename, size, data) + + def stream_object_data(self, ref: str) -> Tuple[str, str, int, 'Git.CatFileContentStream']: + """ As get_object_header, but returns the data as a stream + + :return: (hexsha, type_string, size_as_int, stream) + :note: This method is not threadsafe, you need one independent Command instance per thread to be safe !""" + cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True) + hexsha, typename, size = self.__get_object_header(cmd, ref) + cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() + return (hexsha, typename, size, self.CatFileContentStream(size, cmd_stdout)) + + def clear_cache(self) -> 'Git': + """Clear all kinds of internal caches to release resources. + + Currently persistent commands will be interrupted. + + :return: self""" + for cmd in (self.cat_file_all, self.cat_file_header): + if cmd: + cmd.__del__() + + self.cat_file_all = None + self.cat_file_header = None + return self diff --git a/venv/Lib/site-packages/git/compat.py b/venv/Lib/site-packages/git/compat.py new file mode 100644 index 0000000..988c04e --- /dev/null +++ b/venv/Lib/site-packages/git/compat.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# config.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +"""utilities to help provide compatibility with python 3""" +# flake8: noqa + +import locale +import os +import sys + +from gitdb.utils.encoding import ( + force_bytes, # @UnusedImport + force_text # @UnusedImport +) + +# typing -------------------------------------------------------------------- + +from typing import ( + Any, + AnyStr, + Dict, + IO, + Optional, + Tuple, + Type, + Union, + overload, +) +# --------------------------------------------------------------------------- + + +is_win: bool = (os.name == 'nt') +is_posix = (os.name == 'posix') +is_darwin = (os.name == 'darwin') +defenc = sys.getfilesystemencoding() + + +@overload +def safe_decode(s: None) -> None: ... + + +@overload +def safe_decode(s: AnyStr) -> str: ... + + +def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: + """Safely decodes a binary string to unicode""" + if isinstance(s, str): + return s + elif isinstance(s, bytes): + return s.decode(defenc, 'surrogateescape') + elif s is None: + return None + else: + raise TypeError('Expected bytes or text, but got %r' % (s,)) + + +@overload +def safe_encode(s: None) -> None: ... + + +@overload +def safe_encode(s: AnyStr) -> bytes: ... + + +def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: + """Safely encodes a binary string to unicode""" + if isinstance(s, str): + return s.encode(defenc) + elif isinstance(s, bytes): + return s + elif s is None: + return None + else: + raise TypeError('Expected bytes or text, but got %r' % (s,)) + + +@overload +def win_encode(s: None) -> None: ... + + +@overload +def win_encode(s: AnyStr) -> bytes: ... + + +def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: + """Encode unicodes for process arguments on Windows.""" + if isinstance(s, str): + return s.encode(locale.getpreferredencoding(False)) + elif isinstance(s, bytes): + return s + elif s is not None: + raise TypeError('Expected bytes or text, but got %r' % (s,)) + return None diff --git a/venv/Lib/site-packages/git/config.py b/venv/Lib/site-packages/git/config.py new file mode 100644 index 0000000..cbd6602 --- /dev/null +++ b/venv/Lib/site-packages/git/config.py @@ -0,0 +1,847 @@ +# config.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +"""Module containing module parser implementation able to properly read and write +configuration files""" + +import sys +import abc +from functools import wraps +import inspect +from io import BufferedReader, IOBase +import logging +import os +import re +import fnmatch + +from git.compat import ( + defenc, + force_text, + is_win, +) + +from git.util import LockFile + +import os.path as osp + +import configparser as cp + +# typing------------------------------------------------------- + +from typing import (Any, Callable, Generic, IO, List, Dict, Sequence, + TYPE_CHECKING, Tuple, TypeVar, Union, cast) + +from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T + +if TYPE_CHECKING: + from git.repo.base import Repo + from io import BytesIO + +T_ConfigParser = TypeVar('T_ConfigParser', bound='GitConfigParser') +T_OMD_value = TypeVar('T_OMD_value', str, bytes, int, float, bool) + +if sys.version_info[:3] < (3, 7, 2): + # typing.Ordereddict not added until py 3.7.2 + from collections import OrderedDict + OrderedDict_OMD = OrderedDict +else: + from typing import OrderedDict + OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] + +# ------------------------------------------------------------- + +__all__ = ('GitConfigParser', 'SectionConstraint') + + +log = logging.getLogger('git.config') +log.addHandler(logging.NullHandler()) + +# invariants +# represents the configuration level of a configuration file + + +CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") + + +# Section pattern to detect conditional includes. +# https://git-scm.com/docs/git-config#_conditional_includes +CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") + + +class MetaParserBuilder(abc.ABCMeta): + """Utlity class wrapping base-class methods into decorators that assure read-only properties""" + def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> 'MetaParserBuilder': + """ + Equip all base-class methods with a needs_values decorator, and all non-const methods + with a set_dirty_and_flush_changes decorator in addition to that.""" + kmm = '_mutating_methods_' + if kmm in clsdict: + mutating_methods = clsdict[kmm] + for base in bases: + methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) + for name, method in methods: + if name in clsdict: + continue + method_with_values = needs_values(method) + if name in mutating_methods: + method_with_values = set_dirty_and_flush_changes(method_with_values) + # END mutating methods handling + + clsdict[name] = method_with_values + # END for each name/method pair + # END for each base + # END if mutating methods configuration is set + + new_type = super(MetaParserBuilder, cls).__new__(cls, name, bases, clsdict) + return new_type + + +def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: + """Returns method assuring we read values (on demand) before we try to access them""" + + @wraps(func) + def assure_data_present(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + self.read() + return func(self, *args, **kwargs) + # END wrapper method + return assure_data_present + + +def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]: + """Return method that checks whether given non constant function may be called. + If so, the instance will be set dirty. + Additionally, we flush the changes right to disk""" + + def flush_changes(self: 'GitConfigParser', *args: Any, **kwargs: Any) -> _T: + rval = non_const_func(self, *args, **kwargs) + self._dirty = True + self.write() + return rval + # END wrapper method + flush_changes.__name__ = non_const_func.__name__ + return flush_changes + + +class SectionConstraint(Generic[T_ConfigParser]): + + """Constrains a ConfigParser to only option commands which are constrained to + always use the section we have been initialized with. + + It supports all ConfigParser methods that operate on an option. + + :note: + If used as a context manager, will release the wrapped ConfigParser.""" + __slots__ = ("_config", "_section_name") + _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", + "remove_section", "remove_option", "options") + + def __init__(self, config: T_ConfigParser, section: str) -> None: + self._config = config + self._section_name = section + + def __del__(self) -> None: + # Yes, for some reason, we have to call it explicitly for it to work in PY3 ! + # Apparently __del__ doesn't get call anymore if refcount becomes 0 + # Ridiculous ... . + self._config.release() + + def __getattr__(self, attr: str) -> Any: + if attr in self._valid_attrs_: + return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) + return super(SectionConstraint, self).__getattribute__(attr) + + def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: + """Call the configuration at the given method which must take a section name + as first argument""" + return getattr(self._config, method)(self._section_name, *args, **kwargs) + + @property + def config(self) -> T_ConfigParser: + """return: Configparser instance we constrain""" + return self._config + + def release(self) -> None: + """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" + return self._config.release() + + def __enter__(self) -> 'SectionConstraint[T_ConfigParser]': + self._config.__enter__() + return self + + def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: + self._config.__exit__(exception_type, exception_value, traceback) + + +class _OMD(OrderedDict_OMD): + """Ordered multi-dict.""" + + def __setitem__(self, key: str, value: _T) -> None: + super(_OMD, self).__setitem__(key, [value]) + + def add(self, key: str, value: Any) -> None: + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return None + super(_OMD, self).__getitem__(key).append(value) + + def setall(self, key: str, values: List[_T]) -> None: + super(_OMD, self).__setitem__(key, values) + + def __getitem__(self, key: str) -> Any: + return super(_OMD, self).__getitem__(key)[-1] + + def getlast(self, key: str) -> Any: + return super(_OMD, self).__getitem__(key)[-1] + + def setlast(self, key: str, value: Any) -> None: + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return + + prior = super(_OMD, self).__getitem__(key) + prior[-1] = value + + def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: + return super(_OMD, self).get(key, [default])[-1] + + def getall(self, key: str) -> List[_T]: + return super(_OMD, self).__getitem__(key) + + def items(self) -> List[Tuple[str, _T]]: # type: ignore[override] + """List of (key, last value for key).""" + return [(k, self[k]) for k in self] + + def items_all(self) -> List[Tuple[str, List[_T]]]: + """List of (key, list of values for key).""" + return [(k, self.getall(k)) for k in self] + + +def get_config_path(config_level: Lit_config_levels) -> str: + + # we do not support an absolute path of the gitconfig on windows , + # use the global config instead + if is_win and config_level == "system": + config_level = "global" + + if config_level == "system": + return "/etc/gitconfig" + elif config_level == "user": + config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", '~'), ".config") + return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) + elif config_level == "global": + return osp.normpath(osp.expanduser("~/.gitconfig")) + elif config_level == "repository": + raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") + else: + # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs + assert_never(config_level, # type: ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}")) + + +class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): + + """Implements specifics required to read git style configuration files. + + This variation behaves much like the git.config command such that the configuration + will be read on demand based on the filepath given during initialization. + + The changes will automatically be written once the instance goes out of scope, but + can be triggered manually as well. + + The configuration file will be locked if you intend to change values preventing other + instances to write concurrently. + + :note: + The config is case-sensitive even when queried, hence section and option names + must match perfectly. + If used as a context manager, will release the locked file.""" + + #{ Configuration + # The lock type determines the type of lock to use in new configuration readers. + # They must be compatible to the LockFile interface. + # A suitable alternative would be the BlockingLockFile + t_lock = LockFile + re_comment = re.compile(r'^\s*[#;]') + + #} END configuration + + optvalueonly_source = r'\s*(?P