diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 14bf1024e..f51f7ce13 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -1,966 +1,470 @@ { - "files": { - "./git/cmd.py": [ - { - "code": "reportGeneralTypeIssues", - "range": { - "startColumn": 13, - "endColumn": 32, - "lineCount": 1 - } - }, - { - "code": "reportOptionalOperand", - "range": { - "startColumn": 27, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 28, - "endColumn": 31, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 28, - "endColumn": 40, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 68, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 31, - "lineCount": 1 - } - } - ], - "./git/config.py": [ - { - "code": "reportGeneralTypeIssues", - "range": { - "startColumn": 11, - "endColumn": 26, - "lineCount": 1 - } - }, - { - "code": "reportInvalidTypeVarUse", - "range": { - "startColumn": 43, - "endColumn": 45, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 41, - "endColumn": 48, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 32, - "endColumn": 39, - "lineCount": 1 - } - }, - { - "code": "reportCallIssue", - "range": { - "startColumn": 25, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 30, - "endColumn": 39, - "lineCount": 1 - } - } - ], - "./git/db.py": [ - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 12, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 61, - "endColumn": 79, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 14, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 70, - "endColumn": 88, - "lineCount": 1 - } - } - ], - "./git/index/base.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 30, - "endColumn": 36, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 28, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportAssignmentType", - "range": { - "startColumn": 38, - "endColumn": 76, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 45, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 60, - "endColumn": 63, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 56, - "endColumn": 60, - "lineCount": 1 - } - }, - { - "code": "reportSelfClsParameterName", - "range": { - "startColumn": 30, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 51, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 55, - "endColumn": 61, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 12, - "lineCount": 1 - } - }, - { - "code": "reportAssignmentType", - "range": { - "startColumn": 20, - "endColumn": 46, - "lineCount": 1 - } - } - ], - "./git/index/fun.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 8, - "endColumn": 36, - "lineCount": 1 - } - } - ], - "./git/index/typ.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 46, - "lineCount": 1 - } - } - ], - "./git/objects/base.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 29, - "endColumn": 36, - "lineCount": 1 - } - } - ], - "./git/objects/blob.py": [ - { - "code": "reportIncompatibleVariableOverride", - "range": { - "startColumn": 4, - "endColumn": 8, - "lineCount": 1 - } - } - ], - "./git/objects/commit.py": [ - { - "code": "reportIncompatibleVariableOverride", - "range": { - "startColumn": 4, - "endColumn": 8, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 31, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 30, - "lineCount": 1 - } - } - ], - "./git/objects/submodule/base.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 24, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 24, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 23, - "endColumn": 25, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 23, - "endColumn": 25, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 41, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 52, - "endColumn": 84, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 40, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 35, - "endColumn": 42, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 20, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 20, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 19, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 18, - "endColumn": 20, - "lineCount": 1 - } - } - ], - "./git/objects/tag.py": [ - { - "code": "reportIncompatibleVariableOverride", - "range": { - "startColumn": 4, - "endColumn": 8, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 72, - "endColumn": 78, - "lineCount": 1 - } - } - ], - "./git/objects/tree.py": [ - { - "code": "reportIncompatibleVariableOverride", - "range": { - "startColumn": 4, - "endColumn": 8, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 31, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 19, - "endColumn": 83, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 54, - "lineCount": 1 - } - } - ], - "./git/objects/util.py": [ - { - "code": "reportAssignmentType", - "range": { - "startColumn": 23, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 22, - "endColumn": 26, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 30, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 54, - "lineCount": 1 - } - } - ], - "./git/refs/log.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 30, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 17, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 28, - "endColumn": 30, - "lineCount": 1 - } - } - ], - "./git/refs/reference.py": [ - { - "code": "reportInvalidTypeVarUse", - "range": { - "startColumn": 22, - "endColumn": 34, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleVariableOverride", - "range": { - "startColumn": 13, - "endColumn": 17, - "lineCount": 1 - } - } - ], - "./git/refs/symbolic.py": [ - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 15, - "endColumn": 20, - "lineCount": 1 - } - } - ], - "./git/refs/tag.py": [ - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 14, - "lineCount": 1 - } - }, - { - "code": "reportIncompatibleMethodOverride", - "range": { - "startColumn": 8, - "endColumn": 14, - "lineCount": 1 - } - } - ], - "./git/remote.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 25, - "endColumn": 36, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 29, - "endColumn": 40, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 26, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 26, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 26, - "endColumn": 38, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 26, - "endColumn": 38, - "lineCount": 1 - } - } - ], - "./git/repo/base.py": [ - { - "code": "reportRedeclaration", - "range": { - "startColumn": 4, - "endColumn": 15, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 18, - "endColumn": 22, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 35, - "endColumn": 41, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 46, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 51, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 15, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 16, - "endColumn": 31, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 21, - "endColumn": 31, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 34, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 65, - "endColumn": 79, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 82, - "endColumn": 102, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 50, - "endColumn": 69, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 68, - "endColumn": 85, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 88, - "endColumn": 111, - "lineCount": 1 - } - }, - { - "code": "reportTypedDictNotRequiredAccess", - "range": { - "startColumn": 51, - "endColumn": 73, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 26, - "lineCount": 1 - } - }, - { - "code": "reportAssignmentType", - "range": { - "startColumn": 22, - "endColumn": 38, - "lineCount": 1 - } - } - ], - "./git/repo/fun.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 25, - "endColumn": 33, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 35, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportAssignmentType", - "range": { - "startColumn": 18, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 14, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 24, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 14, - "lineCount": 1 - } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 11, - "endColumn": 20, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 25, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 24, - "endColumn": 27, - "lineCount": 1 - } - } - ], - "./test/deprecation/test_basic.py": [ - { - "code": "reportUnusedExpression", - "range": { - "startColumn": 12, - "endColumn": 62, - "lineCount": 1 - } - } - ] - } + "files": { + "./git/config.py": [ + { + "code": "reportInvalidTypeVarUse", + "range": { + "startColumn": 43, + "endColumn": 45, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 41, + "endColumn": 48, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 32, + "endColumn": 39, + "lineCount": 1 + } + }, + { + "code": "reportCallIssue", + "range": { + "startColumn": 25, + "endColumn": 46, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 30, + "endColumn": 39, + "lineCount": 1 + } + } + ], + "./git/db.py": [ + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 12, + "lineCount": 1 + } + }, + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 14, + "lineCount": 1 + } + } + ], + "./git/index/base.py": [ + { + "code": "reportArgumentType", + "range": { + "startColumn": 30, + "endColumn": 36, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 28, + "endColumn": 34, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 51, + "endColumn": 57, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 55, + "endColumn": 61, + "lineCount": 1 + } + }, + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 12, + "lineCount": 1 + } + }, + { + "code": "reportAssignmentType", + "range": { + "startColumn": 20, + "endColumn": 46, + "lineCount": 1 + } + } + ], + "./git/objects/blob.py": [ + { + "code": "reportIncompatibleVariableOverride", + "range": { + "startColumn": 4, + "endColumn": 8, + "lineCount": 1 + } + } + ], + "./git/objects/commit.py": [ + { + "code": "reportIncompatibleVariableOverride", + "range": { + "startColumn": 4, + "endColumn": 8, + "lineCount": 1 + } + }, + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 31, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 20, + "endColumn": 33, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 20, + "endColumn": 30, + "lineCount": 1 + } + } + ], + "./git/objects/submodule/base.py": [ + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 19, + "endColumn": 24, + "lineCount": 1 + } + }, + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 19, + "endColumn": 24, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 23, + "endColumn": 25, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 23, + "endColumn": 25, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 52, + "endColumn": 84, + "lineCount": 1 + } + } + ], + "./git/objects/tag.py": [ + { + "code": "reportIncompatibleVariableOverride", + "range": { + "startColumn": 4, + "endColumn": 8, + "lineCount": 1 + } + } + ], + "./git/objects/tree.py": [ + { + "code": "reportIncompatibleVariableOverride", + "range": { + "startColumn": 4, + "endColumn": 8, + "lineCount": 1 + } + }, + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 31, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 19, + "endColumn": 83, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 15, + "endColumn": 54, + "lineCount": 1 + } + } + ], + "./git/objects/util.py": [ + { + "code": "reportAssignmentType", + "range": { + "startColumn": 23, + "endColumn": 34, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 22, + "endColumn": 26, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 30, + "endColumn": 34, + "lineCount": 1 + } + }, + { + "code": "reportReturnType", + "range": { + "startColumn": 15, + "endColumn": 54, + "lineCount": 1 + } + } + ], + "./git/refs/log.py": [ + { + "code": "reportArgumentType", + "range": { + "startColumn": 30, + "endColumn": 34, + "lineCount": 1 + } + }, + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 17, + "endColumn": 22, + "lineCount": 1 + } + }, + { + "code": "reportArgumentType", + "range": { + "startColumn": 28, + "endColumn": 30, + "lineCount": 1 + } + } + ], + "./git/refs/reference.py": [ + { + "code": "reportIncompatibleVariableOverride", + "range": { + "startColumn": 13, + "endColumn": 17, + "lineCount": 1 + } + } + ], + "./git/refs/symbolic.py": [ + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 15, + "endColumn": 20, + "lineCount": 1 + } + } + ], + "./git/refs/tag.py": [ + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 14, + "lineCount": 1 + } + }, + { + "code": "reportIncompatibleMethodOverride", + "range": { + "startColumn": 8, + "endColumn": 14, + "lineCount": 1 + } + } + ], + "./git/remote.py": [ + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 26, + "endColumn": 38, + "lineCount": 1 + } + }, + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 26, + "endColumn": 38, + "lineCount": 1 + } + }, + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 26, + "endColumn": 38, + "lineCount": 1 + } + }, + { + "code": "reportAttributeAccessIssue", + "range": { + "startColumn": 26, + "endColumn": 38, + "lineCount": 1 + } + } + ], + "./git/repo/base.py": [ + { + "code": "reportReturnType", + "range": { + "startColumn": 15, + "endColumn": 28, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 21, + "endColumn": 31, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 34, + "endColumn": 44, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 65, + "endColumn": 79, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 82, + "endColumn": 102, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 50, + "endColumn": 69, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 68, + "endColumn": 85, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 88, + "endColumn": 111, + "lineCount": 1 + } + }, + { + "code": "reportTypedDictNotRequiredAccess", + "range": { + "startColumn": 51, + "endColumn": 73, + "lineCount": 1 + } + } + ], + "./git/repo/fun.py": [ + { + "code": "reportReturnType", + "range": { + "startColumn": 11, + "endColumn": 20, + "lineCount": 1 + } + } + ], + "./test/deprecation/test_basic.py": [ + { + "code": "reportUnusedExpression", + "range": { + "startColumn": 12, + "endColumn": 62, + "lineCount": 1 + } + } + ] + } } diff --git a/.github/workflows/alpine-test.yml b/.github/workflows/alpine-test.yml index b10336a15..d87fdd079 100644 --- a/.github/workflows/alpine-test.yml +++ b/.github/workflows/alpine-test.yml @@ -89,7 +89,7 @@ jobs: command -v git python git version python --version - python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' + python -c 'import os, sys; print(f"{sys.platform=}, {os.name=}")' - name: Test with pytest run: | diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index e14856e7e..4f7032f31 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -133,7 +133,7 @@ jobs: command -v git python git version python --version - python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' + python -c 'import os, sys; print(f"{sys.platform=}, {os.name=}")' - name: Test with pytest (${{ matrix.additional-pytest-args }}) run: | diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 4803e733b..b4046dd9b 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -11,12 +11,12 @@ permissions: jobs: test: - runs-on: ${{ matrix.python-version == '3.7' && 'ubuntu-22.04' || 'ubuntu-latest' }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: project: [smmap, gitdb] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"] steps: - uses: actions/checkout@v7 @@ -40,3 +40,18 @@ jobs: else pytest -o addopts= -v smmap/smmap/test fi + uv-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + enable-cache: false + version: "0.12.13" # Released on 2026-09-10 + + # Ensure that dependencies and dependency groups are consistent and resolvable. + - run: uv sync --all-extras --all-groups --dry-run diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7dfa2e8f2..3fbbe2ec6 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,22 +17,14 @@ jobs: strategy: matrix: os-type: [ubuntu, macos, windows] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t"] exclude: - - os-type: macos - python-version: "3.7" # Not available for the ARM-based macOS runners. - os-type: macos python-version: "3.14t" - os-type: macos python-version: "3.15t" include: - - os-ver: latest - - os-type: ubuntu - python-version: "3.7" - os-ver: "22.04" - build-docs: true # We ensure documentation builds, except on very old interpreters. - - python-version: "3.7" - build-docs: false - python-version: "3.8" build-docs: false - experimental: false @@ -43,7 +35,7 @@ jobs: fail-fast: false - runs-on: ${{ matrix.os-type }}-${{ matrix.os-ver }} + runs-on: ${{ matrix.os-type }}-latest defaults: run: @@ -140,7 +132,7 @@ jobs: command -v git python git version python --version - python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' + python -c 'import os, sys; print(f"{sys.platform=}, {os.name=}")' # For debugging hook tests on native Windows systems that may have WSL. - name: Show bash.exe candidates (Windows) @@ -152,7 +144,7 @@ jobs: continue-on-error: true - name: Check types with mypy - if: matrix.python-version != '3.7' && matrix.python-version != '3.8' + if: matrix.python-version != '3.8' run: | mypy --python-version="${PYTHON_VERSION%t}" # Version only, with no "t" for free-threaded. env: @@ -173,5 +165,5 @@ jobs: - name: Documentation if: matrix.build-docs run: | - pip install '.[doc]' + pip install --group doc make -C doc html diff --git a/.gitignore b/.gitignore index eab294a65..79e543121 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ output.txt # Files created by OSS-Fuzz when running locally fuzz_*.pkg.spec + +# Lock files created by uv, pixi, pipenv and others +*.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dfa5fc91e..9471680e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,8 @@ exclude: ^(?:gitdb|smmap)/ repos: -- repo: https://github.com/codespell-project/codespell - rev: v2.4.3 - hooks: - - id: codespell - additional_dependencies: [tomli] - exclude: ^test/fixtures/ - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.5 + rev: v0.16.9 hooks: - id: ruff-check args: ["--fix"] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 9bce80fd2..206d3f492 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,6 +13,10 @@ build: # nodejs: "20" # rust: "1.70" # golang: "1.20" + jobs: + install: + - python -m pip install --upgrade --no-cache-dir pip setuptools + - python -m pip install --upgrade --upgrade-strategy only-if-needed --no-cache-dir --group doc . # Build documentation in the "doc/" directory with Sphinx. sphinx: @@ -24,13 +28,3 @@ sphinx: # Optionally build your docs in additional formats such as PDF and ePub. formats: all - -# Optional but recommended, declare the Python requirements required -# to build your documentation. -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -python: - install: - - method: pip - path: . - extra_requirements: - - doc diff --git a/AGENTS.md b/AGENTS.md index ee4ef9d95..cdffa4764 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,43 @@ +# Contribution guidelines + +Before starting work, read and follow [CONTRIBUTING.md](CONTRIBUTING.md), +including the [Prevent agent impersonation](CONTRIBUTING.md#prevent-agent-impersonation) +section governing agent identification and separation of unaltered user statements +when communicating through a person's account. + # Commit messages -Every commit must have a descriptive title and a substantive body. Title-only -commit messages are not acceptable. +Follow Conventional Commits for every commit. Every commit must have a +descriptive title and a substantive body. Title-only commit messages are not +acceptable. + +## Formatting + +Write commit messages in Markdown and assume readers view them with syntax +highlighting. Enclose code identifiers, package and module names, file paths, +and shell commands in backticks. Use Markdown whenever it helps readers +understand or navigate the prose. + +## Titles + +- Every title must use the form `type: description` or + `type(scope): description`. +- Use `feat:` for user-visible features and `fix:` for user-visible fixes. +- Use appropriate prefixes for other changes, such as `docs:`, `test:`, `ci:`, + `build:`, `refactor:`, `perf:`, `style:`, or `chore:`. +- Breaking changes must use `!` immediately before the colon, for example + `feat!:`, `refactor!:`, or a scoped form such as `fix(repo)!:`. +- Optionally scope a commit to the affected component, for example `fix(repo):`. + +Example titles: + +- `feat: add support for a new Git option` +- `fix(repo): handle bare repositories correctly` +- `build!: drop support for an older Python version` +- `ci: add an independent documentation build` +- `refactor(repo): simplify repository initialization` + +## Body The body must explain the problem or motivation, what changed, and why the chosen approach addresses it. Include relevant behavior before and after the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76f276323..1a64a701d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,13 @@ example in issue or PR descriptions and comments. AI assistance that does not re the person as the speaker, such as proofreading or wording polish, does not require identification. +Even when identifying themselves, agents must not assert on a person's behalf that +that person performed an action, such as reviewing or approving a PR. The person +must make any such statement themselves. If it is included alongside agent-authored +content, it must be supplied by the person and preserved verbatim in a clearly +labeled, separate user-authored section. Agents must not draft, paraphrase, or embed +such statements in their own narration. + Attributing AI assistance in commit metadata, for example with a `Co-authored-by` trailer, is welcome but not required. Code is reviewed the same way regardless of its origin. diff --git a/README.md b/README.md index c5a069bfe..029b4fa00 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ GitPython needs the `git` executable to be installed on the system and available by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. - Git (1.7.x or newer) -- Python >= 3.7 +- Python >= 3.8 The list of dependencies are listed in [`./requirements.txt`](https://github.com/gitpython-developers/GitPython/blob/main/requirements.txt) and [`./test-requirements.txt`](https://github.com/gitpython-developers/GitPython/blob/main/test-requirements.txt). The installer takes care of installing them for you. diff --git a/VERSION b/VERSION index 4eb2ee669..e4604e3af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.62 +3.2.1 diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 24472ba39..000000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx >= 7.4.7, < 8 -sphinx_rtd_theme -sphinx-autodoc-typehints diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 20bfaeae6..95178088d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,44 @@ Changelog ========= +3.2.1 +===== + +Security fixes for + +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-83vg-56qc-22m7 + +If you can, also try and provide feedback on the upcoming v4 branch +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. + +See the following for all changes. +https://github.com/gitpython-developers/GitPython/releases/tag/3.2.1 + +3.2.0 +===== + +Drops support for Python 3.7 (5 years after its EOL). + +Security fixes for + +* Deprecate the pure-Python ``GitDB`` object database backend due to security and + performance issues. Selecting it or a subclass through ``odbt`` now emits a + ``DeprecationWarning``. Remove ``odbt=GitDB`` to use ``GitCmdObjectDB``, the + existing default, or select ``odbt=GitCmdObjectDB`` explicitly. The ``gitdb`` + package remains a dependency for shared types and utilities. +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-gq48-pqfc-9p58 +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-23mf-xhv8-69c2 +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-f9j4-qggq-h239 +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-w8jc-g24h-crhw +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-m64x-33q8-m5h7 +* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-fx3j-rwgx-fr94 + +If you can, also try and provide feedback on the upcoming v4 branch +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. + +See the following for all changes. +https://github.com/gitpython-developers/GitPython/releases/tag/3.2.0 + 3.1.62 ====== @@ -10,7 +48,7 @@ Security fixes for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-59cr-6r3x-644w If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.62 @@ -22,7 +60,7 @@ A fixup release to avoid accidental removal of public class regex on Actor. It's now deprecated instead. If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.61 @@ -37,7 +75,7 @@ Security fixes for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-239g-whfq-7xj9 If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.60 @@ -54,7 +92,7 @@ Security fixes for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-7833-fr7j-v32q If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.59 @@ -72,7 +110,7 @@ Security fixes for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hmq2-w58f-27jc If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.58 @@ -86,7 +124,7 @@ A security fix for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-539m-9xh6-q6rr If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.57 @@ -99,7 +137,7 @@ A security fix for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-p538-c434-8v24 If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.56 @@ -112,7 +150,7 @@ A security fix for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-94p4-4cq8-9g67 If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.55 @@ -127,7 +165,7 @@ A security fix for * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-fjr4-x663-mwxc If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.54 @@ -139,7 +177,7 @@ A security fix for https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3rp5-jjmw-4wv2 If you can, also try and provide feedback on the upcoming v4 branch -https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. +https://github.com/gitpython-developers/GitPython/pull/2274 - patches welcome. See the following for all changes. https://github.com/gitpython-developers/GitPython/releases/tag/3.1.53 diff --git a/doc/source/intro.rst b/doc/source/intro.rst index e1075b1c9..dec2ec0fa 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -13,7 +13,7 @@ The object database implementation is optimized for handling large quantities of Requirements ============ -* `Python`_ >= 3.7 +* `Python`_ >= 3.8 * `Git`_ 1.7.0 or newer It should also work with older versions, but it may be that some operations involving remotes will not work as expected. diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index d095d3be3..a1ac1eace 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -78,6 +78,12 @@ Query relevant repository paths ... :class:`Heads ` Heads are branches in git-speak. :class:`References ` are pointers to a specific commit or to other references. Heads and :class:`Tags ` are a kind of references. GitPython allows you to query them rather intuitively. +To obtain the current commit ID, use ``repo.head.commit.hexsha``. This works both +on a branch and with a detached HEAD, provided HEAD resolves to an existing commit. +When ``repo.head.is_detached`` is true, HEAD points directly to a commit and there +is no active branch: reading ``repo.head.reference`` or ``repo.active_branch`` +raises :exc:`TypeError`. The branch examples below assume an attached HEAD. + .. literalinclude:: ../../test/test_docs.py :language: python :dedent: 8 @@ -152,7 +158,7 @@ Examining References :start-after: # [2-test_references_and_objects] :end-before: # ![2-test_references_and_objects] -A :class:`symbolic reference ` is a special case of a reference as it points to another reference instead of a commit. +A :class:`symbolic reference ` can point to another reference. When detached, it points directly to a commit instead. Reading its ``commit`` property resolves the commit in either state. Assigning a commit to ``reference`` detaches it; reading ``reference`` then raises :exc:`TypeError`. .. literalinclude:: ../../test/test_docs.py :language: python @@ -507,24 +513,29 @@ Object Databases The type of the database determines certain performance characteristics, such as the quantity of objects that can be read per second, the resource usage when reading large data files, as well as the average memory footprint of your application. -GitDB -===== -The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. It uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities of small objects from densely packed repositories:: +GitCmdObjectDB +============== +``GitCmdObjectDB`` is the default and recommended backend. It reads objects and +resolves abbreviated object IDs through persistent ``git cat-file`` processes:: - repo = Repo("path/to/repo", odbt=GitDB) + repo = Repo("path/to/repo") + # Equivalent explicit selection: + repo = Repo("path/to/repo", odbt=GitCmdObjectDB) +GitDB +===== .. warning:: - ``GitDB`` may fail or become extremely slow when traversing trees in - repositories with very large commits (thousands of changed files in a - single commit). If you encounter ``RecursionError`` or excessive - slowness during tree traversal, switch to ``GitCmdObjectDB`` instead. - + The pure-Python ``GitDB`` backend is deprecated due to security and performance + issues. Its object parsers can exhaust resources or return incorrect object + data when processing untrusted repositories. Do not use it for untrusted data. -GitCmdObjectDB -============== -The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than ``GitDB``:: +Selecting ``odbt=GitDB`` (including a subclass) emits a ``DeprecationWarning``. +To migrate, remove ``odbt=GitDB`` or replace it with ``odbt=GitCmdObjectDB`` when +opening, initializing, or cloning a repository. The deprecated backend remains +available for compatibility; deprecation does not fix its parsing issues. - repo = Repo("path/to/repo", odbt=GitCmdObjectDB) +The ``gitdb`` package remains a dependency because GitPython still uses its shared +types and utilities. Git Command Debugging and Customization *************************************** diff --git a/fuzzing/fuzz-targets/utils.py b/fuzzing/fuzz-targets/utils.py index 97e6eab98..34b05f7eb 100644 --- a/fuzzing/fuzz-targets/utils.py +++ b/fuzzing/fuzz-targets/utils.py @@ -49,7 +49,7 @@ def read_lines_from_file(file_path: str) -> list: except FileNotFoundError: print(f"File not found: {file_path}") return [] - except IOError as e: + except OSError as e: print(f"Error reading file {file_path}: {e}") return [] diff --git a/git/__init__.py b/git/__init__.py index 1b2360e3a..ecc6cd94e 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -295,6 +295,6 @@ def refresh(path: Optional[PathLike] = None) -> None: try: refresh() except Exception as _exc: - raise ImportError("Failed to initialize: {0}".format(_exc)) from _exc + raise ImportError(f"Failed to initialize: {_exc}") from _exc # } END initialize git executable path diff --git a/git/cmd.py b/git/cmd.py index 193dfd4f6..1d1ca909a 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -15,44 +15,27 @@ import re import signal import subprocess -from subprocess import DEVNULL, PIPE, Popen import sys -from textwrap import dedent import threading +import time import warnings - -from git.compat import defenc, force_bytes, safe_decode -from git.exc import ( - CommandError, - GitCommandError, - GitCommandNotFound, - UnsafeOptionError, - UnsafeProtocolError, -) -from git.util import ( - cygpath, - expand_path, - is_cygwin_git, - patch_env, - remove_password_if_present, - stream_copy, -) +from subprocess import DEVNULL, PIPE, Popen +from textwrap import dedent # typing --------------------------------------------------------------------------- - from typing import ( + IO, + TYPE_CHECKING, Any, AnyStr, BinaryIO, Callable, Dict, - IO, Iterator, List, Mapping, Optional, Sequence, - TYPE_CHECKING, TextIO, Tuple, Union, @@ -60,12 +43,29 @@ overload, ) +from git.compat import defenc, force_bytes, safe_decode +from git.exc import ( + CommandError, + GitCommandError, + GitCommandNotFound, + UnsafeOptionError, + UnsafeProtocolError, +) +from git.util import ( + cygpath, + expand_path, + is_cygwin_git, + patch_env, + remove_password_if_present, + stream_copy, +) + if sys.version_info >= (3, 10): from typing import TypeAlias else: from typing_extensions import TypeAlias -from git.types import Literal, PathLike, TBD +from git.types import TBD, Literal, PathLike if TYPE_CHECKING: from git.diff import DiffIndex @@ -99,8 +99,47 @@ ## @{ +def _kill_process(pid: int) -> bool: + """Kill a POSIX process and its descendants, returning whether it was killed.""" + # Collect descendants before signalling, while their parent PIDs still identify them. + pids = [pid] + for parent_pid in pids: + try: + try: + p = Popen(["pgrep", "-P", str(parent_pid)], stdout=PIPE) + except FileNotFoundError: + # POSIX ps does not support selecting by parent PID. + ps_args = ["ps", "-ef"] if sys.platform == "cygwin" else ["ps", "-A", "-o", "pid=", "-o", "ppid="] + with Popen(ps_args, stdout=PIPE) as p: + if p.stdout is not None: + for line in p.stdout: + fields = line.split() + if sys.platform == "cygwin": + fields = fields[1:3] # ps -ef starts with UID, PID, PPID. + if len(fields) == 2 and all(field.isdigit() for field in fields): + if int(fields[1]) == parent_pid: + pids.append(int(fields[0])) + else: + with p: + if p.stdout is not None: + for line in p.stdout: + if line.strip().isdigit(): + pids.append(int(line)) + except OSError as ex: + _logger.info("Unable to enumerate child processes: %r", ex) + killed = False + if sys.platform != "win32": + for process_pid in pids: + try: + os.kill(process_pid, signal.SIGKILL) + killed = killed or process_pid == pid + except OSError: + pass + return killed + + def handle_process_output( - process: "Git.AutoInterrupt" | Popen, + process: Union["Git.AutoInterrupt", Popen], stdout_handler: Union[ None, Callable[[AnyStr], None], @@ -165,7 +204,7 @@ def pump_stream( except Exception as ex: _logger.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): + if not (isinstance(ex, ValueError) and stream.closed): # 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: @@ -199,28 +238,26 @@ def pump_stream( t.start() threads.append(t) - # FIXME: Why join? Will block if stdin needs feeding... + # Wait for output handlers to finish before finalizing. + # If the child needs stdin, the caller must arrange to feed/close it + # before this call or concurrently; this function only drains output. + deadline = None if kill_after_timeout is None else time.monotonic() + kill_after_timeout for t in threads: - t.join(timeout=kill_after_timeout) + t.join(timeout=None if deadline is None else max(0, deadline - time.monotonic())) if t.is_alive(): if isinstance(process, Git.AutoInterrupt): + if sys.platform != "win32" and process.proc is not None and process.proc.poll() is None: + _kill_process(process.proc.pid) 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] = ( - f"error: process killed because it timed out. 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[arg-type] + process._timeout_error = ( + f"error: process killed because it timed out. kill_after_timeout={kill_after_timeout} seconds" + ) + break if finalizer: finalizer(process) @@ -326,7 +363,7 @@ class _AutoInterrupt: raise. """ - __slots__ = ("proc", "args", "status") + __slots__ = ("proc", "args", "status", "_timeout_error") # If this is non-zero it will override any status code during _terminate, used # to prevent race conditions in testing. @@ -336,6 +373,7 @@ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: self.proc = proc self.args = args self.status: Union[int, None] = None + self._timeout_error: Optional[str] = None def _terminate(self) -> None: """Terminate the underlying process.""" @@ -344,37 +382,45 @@ def _terminate(self) -> None: 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() + if proc.stdin: + # A timed-out process may already have exited before input is flushed. + with contextlib.suppress(BrokenPipeError): + proc.stdin.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 + except OSError as ex: + _logger.info("Ignored error after process had died: %r", ex) + + # It can be that nothing really exists anymore... + if getattr(os, "kill", None) is None: return - except OSError as ex: - _logger.info("Ignored error after process had died: %r", ex) - # It can be that nothing really exists anymore... - if os is None or getattr(os, "kill", None) is None: - return + # Try to kill it. + try: + proc.terminate() + except (OSError, AttributeError) as ex: + # On interpreter shutdown (notably on Windows), parts of the stdlib used by + # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None), + # which can cause AttributeError during terminate(). In that case, we prefer + # to silently ignore to avoid noisy "Exception ignored in: __del__" messages. + _logger.info("Ignored error while terminating process: %r", ex) + return + # END exception handling + finally: + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() - # Try to kill it. try: - proc.terminate() - status = proc.wait() # Ensure the process goes away. - + status = proc.wait() self.status = self._status_code_if_terminate or status except (OSError, AttributeError) as ex: - # On interpreter shutdown (notably on Windows), parts of the stdlib used by - # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None), - # which can cause AttributeError during terminate(). In that case, we prefer - # to silently ignore to avoid noisy "Exception ignored in: __del__" messages. - _logger.info("Ignored error while terminating process: %r", ex) - # END exception handling + _logger.info("Ignored error while waiting for terminated process: %r", ex) def __del__(self) -> None: self._terminate() @@ -393,11 +439,8 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: May deadlock if output or error pipes are used and not handled separately. :raise git.exc.GitCommandError: - If the return status is not 0. + If the return status is not 0 or output handling timed out. """ - 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() @@ -406,6 +449,11 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: status = self.status p_stderr = None + stderr_b = force_bytes(data=stderr, encoding="utf-8") or b"" + if self._timeout_error is not None: + stderr_b += (b"\n" if stderr_b else b"") + self._timeout_error.encode("utf-8") + status = status or 1 + def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> bytes: if stream: try: @@ -647,7 +695,9 @@ class Git(metaclass=_GitMeta): "_version_info_token", ) - re_unsafe_protocol = re.compile(r"(.+)::.+") + # Match Git's leading transport selector, including an empty helper name. + # Git also selects the command-executing ext helper for an ext:// URL. + re_unsafe_protocol = re.compile(r"([A-Za-z0-9][A-Za-z0-9+.-]*|)::|ext://") unsafe_git_ls_remote_options = [ # This option allows arbitrary command execution in git-ls-remote. @@ -947,7 +997,8 @@ def check_unsafe_protocols(cls, url: str) -> None: Apart from the usual protocols (http, git, ssh), Git allows "remote helpers" that have the form ``::
``. One of these helpers (``ext::``) - can be used to invoke any arbitrary command. + can be used to invoke any arbitrary command. Git also selects that helper + for ``ext://`` URLs and interprets the URL as a command path. See: @@ -956,11 +1007,19 @@ def check_unsafe_protocols(cls, url: str) -> None: """ match = cls.re_unsafe_protocol.match(url) if match: - protocol = match.group(1) + protocol = match.group(0) raise UnsafeProtocolError( - f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." + f"The `{protocol}` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." ) + def _check_unsafe_protocols_in_args(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> None: + """Check positional operands and standalone values in rendered command options. + + A short flag's split value can become the repository operand before ``--``. + """ + for arg in self._unpack_args(args) + self.transform_kwargs(**kwargs): + self.check_unsafe_protocols(arg) + @classmethod def _canonicalize_option_name(cls, option: str) -> str: """Return the option name used for unsafe-option checks. @@ -1131,16 +1190,23 @@ def ls_remote( self, *args: Any, allow_unsafe_options: bool = False, + allow_unsafe_protocols: bool = False, **kwargs: Any, ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: """List references in a remote repository. :param allow_unsafe_options: Allow unsafe options, like ``--upload-pack`` or ``--exec``. + + :param allow_unsafe_protocols: + Allow unsafe protocols to be used, like ``ext``. Positional arguments + and split short-option values are checked. """ if not allow_unsafe_options: candidate_options = self._option_candidates(args, kwargs) Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options) + if not allow_unsafe_protocols: + self._check_unsafe_protocols_in_args(args, kwargs) return self._call_process("ls_remote", *args, **kwargs) @property @@ -1180,52 +1246,93 @@ def version_info(self) -> Tuple[int, ...]: def execute( self, command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, *, as_process: Literal[True], + **subprocess_kwargs: Any, ) -> "AutoInterrupt": ... @overload def execute( self, command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, *, as_process: Literal[False] = False, - stdout_as_string: Literal[True], - ) -> Union[str, Tuple[int, str, str]]: ... + with_extended_output: Literal[False] = False, + stdout_as_string: Literal[True] = True, + with_stdout: Literal[True] = True, + **subprocess_kwargs: Any, + ) -> str: ... @overload def execute( self, command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, *, as_process: Literal[False] = False, - stdout_as_string: Literal[False] = False, - ) -> Union[bytes, Tuple[int, bytes, str]]: ... + with_extended_output: Literal[False] = False, + stdout_as_string: Literal[False], + universal_newlines: Literal[False] = False, + with_stdout: Literal[True] = True, + **subprocess_kwargs: Any, + ) -> bytes: ... @overload def execute( self, command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, *, - with_extended_output: Literal[False], - as_process: Literal[False], - stdout_as_string: Literal[True], - ) -> str: ... + as_process: Literal[False] = False, + with_extended_output: Literal[True], + stdout_as_string: Literal[True] = True, + with_stdout: Literal[True] = True, + **subprocess_kwargs: Any, + ) -> Tuple[int, str, str]: ... @overload def execute( self, command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, *, - with_extended_output: Literal[False], - as_process: Literal[False], + as_process: Literal[False] = False, + with_extended_output: Literal[True], stdout_as_string: Literal[False], - ) -> bytes: ... + universal_newlines: Literal[False] = False, + with_stdout: Literal[True] = True, + **subprocess_kwargs: Any, + ) -> Tuple[int, bytes, str]: ... + @overload def execute( self, command: Union[str, Sequence[Any]], - istream: Union[None, BinaryIO] = None, + istream: Union[None, int, BinaryIO] = None, + *, + as_process: Literal[False] = False, + with_extended_output: Literal[True], + **subprocess_kwargs: Any, + ) -> Tuple[int, Union[str, bytes, None], str]: ... + + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, + *, + as_process: Literal[False] = False, + with_extended_output: Literal[False] = False, + **subprocess_kwargs: Any, + ) -> Union[str, bytes, None]: ... + + @overload + def execute( + self, + command: Union[str, Sequence[Any]], + istream: Union[None, int, BinaryIO] = None, with_extended_output: bool = False, with_exceptions: bool = True, as_process: bool = False, @@ -1239,7 +1346,26 @@ def execute( max_chunk_size: int = io.DEFAULT_BUFFER_SIZE, strip_newline_in_stdout: bool = True, **subprocess_kwargs: Any, - ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: + ) -> Union[None, str, bytes, Tuple[int, Union[str, bytes, None], str], AutoInterrupt]: ... + + def execute( + self, + command: Union[str, Sequence[Any]], + istream: Union[None, int, 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, + strip_newline_in_stdout: bool = True, + **subprocess_kwargs: Any, + ) -> Union[None, str, bytes, Tuple[int, Union[str, bytes, None], str], AutoInterrupt]: R"""Handle executing the command, and consume and return the returned information (stdout). @@ -1303,11 +1429,12 @@ def execute( carefully considered, due to the following limitations: 1. This feature is not supported at all on Windows. - 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to - enumerate child processes, which is available on most GNU/Linux systems - but not most others. - 3. Deeper descendants do not receive signals, though they may sometimes - terminate as a consequence of their parent processes being killed. + 2. Enumerating child processes requires ``pgrep -P``, or a ``ps`` command + supporting the POSIX ``-A`` and ``-o`` options if ``pgrep`` is not + installed (``ps -ef`` on Cygwin). Effectiveness may vary on systems + without these commands. + 3. Descendants are enumerated before signalling. Processes that detach + or spawn after enumeration may not receive signals. 4. `kill_after_timeout` uses ``SIGKILL``, which can have negative side effects on a repository. For example, stale locks in case of :manpage:`git-gc(1)` could render the repository incapable of accepting @@ -1461,39 +1588,15 @@ def execute( timeout = kill_after_timeout def kill_process(pid: int) -> None: - """Callback to kill a process. - - This callback implementation would be ineffective and unsafe on Windows. - """ - p = Popen(["ps", "--ppid", str(pid)], stdout=PIPE) - 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: - os.kill(pid, signal.SIGKILL) - for child_pid in child_pids: - try: - os.kill(child_pid, signal.SIGKILL) - except OSError: - pass - # Tell the main routine that the process was killed. + if _kill_process(pid): assert kill_check is not None kill_check.set() - 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 def make_timeout_error() -> Union[str, bytes]: err = f'Timeout: the command "{" ".join(redacted_command)}" did not complete in {timeout:g} secs.' return err if universal_newlines else err.encode(defenc) - def communicate() -> Tuple[AnyStr, AnyStr]: + def communicate() -> Tuple[Union[str, bytes, None], Union[str, bytes, None]]: assert watchdog is not None assert kill_check is not None watchdog.start() @@ -1513,8 +1616,8 @@ def communicate() -> Tuple[AnyStr, AnyStr]: # Wait for the process to return. status = 0 - stdout_value: Union[str, bytes] = b"" - stderr_value: Union[str, bytes] = b"" + stdout_value: Union[str, bytes, None] = b"" + stderr_value: Union[str, bytes, None] = b"" newline = "\n" if universal_newlines else b"\n" try: if output_stream is None: @@ -1556,7 +1659,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]: if self.GIT_PYTHON_TRACE == "full": cmdstr = " ".join(redacted_command) - def as_text(stdout_value: Union[bytes, str]) -> str: + def as_text(stdout_value: Union[bytes, str, None]) -> str: return not output_stream and safe_decode(stdout_value) or "" # END as_text @@ -1581,6 +1684,8 @@ def as_text(stdout_value: Union[bytes, str]) -> str: if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream. stdout_value = safe_decode(stdout_value) + # stderr is always captured through PIPE. + assert stderr_value is not None # Allow access to the command's status code. if with_extended_output: return (status, stdout_value, safe_decode(stderr_value)) @@ -1725,6 +1830,12 @@ def _call_process( This allows your commands to call git more conveniently, as ``None`` is realized as non-existent. + Positional arguments may intentionally contain command options. Higher-level + APIs must separate their operands with ``--`` where the Git command supports + it, or reject option-shaped operands where Git reparses them internally (for + example, ``pull`` and ``remote update``). Shell quoting cannot prevent Git + from interpreting a leading-dash argument as an option. + :param kwargs: Contains key-values for the following: @@ -1819,7 +1930,7 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: raise ValueError("Failed to parse header: %r" % header_line) return (tokens[0], tokens[1], int(tokens[2])) - def _prepare_ref(self, ref: AnyStr) -> bytes: + def _prepare_ref(self, ref: object) -> 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. @@ -1829,9 +1940,15 @@ def _prepare_ref(self, ref: AnyStr) -> bytes: else: refstr = ref - if not refstr.endswith("\n"): - refstr += "\n" - return refstr.encode(defenc) + # A line feed terminates a request, so one object name must be one line. An + # embedded one would queue a second request on the persistent command while + # only one response line is read back, leaving every later call one response + # behind, answered with the header of an object it did not ask for. + if refstr.endswith("\n"): + refstr = refstr[:-1] + if "\n" in refstr: + raise ValueError("Object name %r contains a line feed" % refstr) + return (refstr + "\n").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) @@ -1846,7 +1963,7 @@ def _get_persistent_cmd(self, attr_name: str, cmd_name: str, *args: Any, **kwarg cmd = cast("Git.AutoInterrupt", cmd) return cmd - def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[str, str, int]: + def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: Union[str, bytes]) -> Tuple[str, str, int]: if cmd.stdin and cmd.stdout: cmd.stdin.write(self._prepare_ref(ref)) cmd.stdin.flush() @@ -1854,7 +1971,7 @@ def __get_object_header(self, cmd: "Git.AutoInterrupt", ref: AnyStr) -> Tuple[st else: raise ValueError("cmd stdin was empty") - def get_object_header(self, ref: str) -> Tuple[str, str, int]: + def get_object_header(self, ref: Union[str, bytes]) -> Tuple[str, str, int]: """Use this method to quickly examine the type and size of the object behind the given ref. @@ -1868,7 +1985,7 @@ def get_object_header(self, ref: str) -> Tuple[str, str, 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]: + def get_object_data(self, ref: Union[str, bytes]) -> Tuple[str, str, int, bytes]: """Similar to :meth:`get_object_header`, but returns object data as well. :return: @@ -1882,7 +1999,7 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: del stream return (hexsha, typename, size, data) - def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileContentStream"]: + def stream_object_data(self, ref: Union[str, bytes]) -> Tuple[str, str, int, "Git.CatFileContentStream"]: """Similar to :meth:`get_object_data`, but returns the data as a stream. :return: diff --git a/git/config.py b/git/config.py index aef881d2e..7109c09c3 100644 --- a/git/config.py +++ b/git/config.py @@ -10,36 +10,35 @@ import abc import configparser as cp import fnmatch -from functools import wraps import inspect -from io import BufferedReader, IOBase import logging import os import os.path as osp import re import sys - -from git.compat import defenc, force_text -from git.util import LockFile +from functools import wraps +from io import BufferedReader, IOBase # typing------------------------------------------------------- - from typing import ( + IO, + TYPE_CHECKING, Any, Callable, + Dict, Generic, - IO, List, - Dict, + OrderedDict, Sequence, - TYPE_CHECKING, Tuple, TypeVar, Union, cast, ) -from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T +from git.compat import defenc, force_text +from git.types import _T, ConfigLevels_Tup, Lit_config_levels, PathLike, assert_never +from git.util import LockFile if TYPE_CHECKING: from io import BytesIO @@ -47,17 +46,9 @@ from git.repo.base import Repo 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 Python 3.7.2. - from collections import OrderedDict +T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool, None) - OrderedDict_OMD = OrderedDict -else: - from typing import OrderedDict - - OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] +OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # ------------------------------------------------------------- @@ -66,7 +57,7 @@ CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") """The configuration level of a configuration file.""" -CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"") +CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeif )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"") """Section pattern to detect conditional includes. See: https://git-scm.com/docs/git-config#_conditional_includes @@ -205,41 +196,67 @@ def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> self._config.__exit__(exception_type, exception_value, traceback) +def _normalize_name(name: str) -> str: + """Fold section and option names, leaving quoted subsections unchanged.""" + prefix, separator, subsection = name.partition('"') + return prefix.lower() + separator + subsection + + class _OMD(OrderedDict_OMD): - """Ordered multi-dict.""" + """Ordered multi-dict matching config names while retaining their first spelling.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + self._keymap: Dict[str, str] = {} + super().__init__(*args, **kwargs) + + def _key(self, key: str) -> str: + stored = self._keymap.get(_normalize_name(key), key) + return stored if super().__contains__(stored) else key + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and super().__contains__(self._key(key)) + + def __delitem__(self, key: str) -> None: + super().__delitem__(self._key(key)) + del self._keymap[_normalize_name(key)] def __setitem__(self, key: str, value: _T) -> None: - super().__setitem__(key, [value]) + self.setall(key, [value]) + + def clear(self) -> None: + super().clear() + self._keymap.clear() def add(self, key: str, value: Any) -> None: if key not in self: - super().__setitem__(key, [value]) + self[key] = value return - super().__getitem__(key).append(value) + self.getall(key).append(value) def setall(self, key: str, values: List[_T]) -> None: + key = self._key(key) super().__setitem__(key, values) + self._keymap[_normalize_name(key)] = key def __getitem__(self, key: str) -> Any: - return super().__getitem__(key)[-1] + return super().__getitem__(self._key(key))[-1] def getlast(self, key: str) -> Any: - return super().__getitem__(key)[-1] + return self[key] def setlast(self, key: str, value: Any) -> None: if key not in self: - super().__setitem__(key, [value]) + self[key] = value return - prior = super().__getitem__(key) - prior[-1] = value + self.getall(key)[-1] = value - def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: - return super().get(key, [default])[-1] + def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: # type: ignore[override] + return super().get(self._key(key), [default])[-1] def getall(self, key: str) -> List[_T]: - return super().__getitem__(key) + return super().__getitem__(self._key(key)) def items(self) -> List[Tuple[str, _T]]: # type: ignore[override] """List of (key, last value for key).""" @@ -288,11 +305,18 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): other instances to write concurrently. :note: - The config is case-sensitive even when queried, hence section and option names - must match perfectly. + Section and option names are case-insensitive; quoted subsection names are + case-sensitive. Names retain their first spelling when enumerated or written. + Case variants are merged, preserving all values in the order they are read. :note: If used as a context manager, this will release the locked file. + + :note: + Options without a value are stored as ``None`` and written without ``=``. + :meth:`get_value` and :meth:`get_values` return an empty string for them, + while :meth:`getboolean` returns ``True``. An explicit empty value is + stored as an empty string and reads as ``False`` with :meth:`getboolean`. """ # { Configuration @@ -306,11 +330,15 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): re_comment = re.compile(r"^\s*[#;]") # } END configuration - optvalueonly_source = r"\s*(?P