indent 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exponent/__init__.py +34 -0
- exponent/cli.py +110 -0
- exponent/commands/cloud_commands.py +585 -0
- exponent/commands/common.py +411 -0
- exponent/commands/config_commands.py +334 -0
- exponent/commands/run_commands.py +222 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +146 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +61 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/mutations.py +160 -0
- exponent/core/graphql/queries.py +146 -0
- exponent/core/graphql/subscriptions.py +16 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +499 -0
- exponent/core/remote_execution/client.py +999 -0
- exponent/core/remote_execution/code_execution.py +77 -0
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/error_info.py +45 -0
- exponent/core/remote_execution/exceptions.py +10 -0
- exponent/core/remote_execution/file_write.py +35 -0
- exponent/core/remote_execution/files.py +330 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/http_fetch.py +94 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +226 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +26 -0
- exponent/core/remote_execution/terminal_session.py +375 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +595 -0
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +296 -0
- exponent/core/remote_execution/types.py +635 -0
- exponent/core/remote_execution/utils.py +477 -0
- exponent/core/types/__init__.py +0 -0
- exponent/core/types/command_data.py +206 -0
- exponent/core/types/event_types.py +89 -0
- exponent/core/types/generated/__init__.py +0 -0
- exponent/core/types/generated/strategy_info.py +213 -0
- exponent/migration-docs/login.md +112 -0
- exponent/py.typed +4 -0
- exponent/utils/__init__.py +0 -0
- exponent/utils/colors.py +92 -0
- exponent/utils/version.py +289 -0
- indent-0.1.26.dist-info/METADATA +38 -0
- indent-0.1.26.dist-info/RECORD +55 -0
- indent-0.1.26.dist-info/WHEEL +4 -0
- indent-0.1.26.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
HALT_CHAT_STREAM_MUTATION: str = """
|
|
2
|
+
mutation HaltChatStream($chatUuid: UUID!) {
|
|
3
|
+
haltChatStream(chatUuid: $chatUuid) {
|
|
4
|
+
__typename
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SET_LOGIN_COMPLETE_MUTATION: str = """
|
|
11
|
+
mutation SetLoginComplete {
|
|
12
|
+
setLoginComplete {
|
|
13
|
+
__typename
|
|
14
|
+
... on User {
|
|
15
|
+
userApiKey
|
|
16
|
+
}
|
|
17
|
+
... on UnauthenticatedError {
|
|
18
|
+
message
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
REFRESH_API_KEY_MUTATION = """
|
|
26
|
+
mutation RefreshApiKey {
|
|
27
|
+
refreshApiKey {
|
|
28
|
+
... on User {
|
|
29
|
+
userApiKey
|
|
30
|
+
}
|
|
31
|
+
... on UnauthenticatedError {
|
|
32
|
+
message
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
START_CHAT_TURN_MUTATION = """
|
|
39
|
+
mutation StartChatTurnMutation($chatInput: ChatInput!, $parentUuid: String, $chatConfig: ChatConfig!) {
|
|
40
|
+
startChatReply(
|
|
41
|
+
chatInput: $chatInput,
|
|
42
|
+
parentUuid: $parentUuid,
|
|
43
|
+
chatConfig: $chatConfig
|
|
44
|
+
) {
|
|
45
|
+
__typename
|
|
46
|
+
... on UnauthenticatedError {
|
|
47
|
+
message
|
|
48
|
+
}
|
|
49
|
+
... on ChatNotFoundError {
|
|
50
|
+
message
|
|
51
|
+
}
|
|
52
|
+
... on Chat {
|
|
53
|
+
chatUuid
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
CREATE_CLOUD_CHAT_MUTATION = """
|
|
61
|
+
mutation CreateCloudChat($configId: String!) {
|
|
62
|
+
createCloudChat(cloudConfigUuid: $configId) {
|
|
63
|
+
__typename
|
|
64
|
+
...on Chat {
|
|
65
|
+
chatUuid
|
|
66
|
+
}
|
|
67
|
+
...on CloudSessionError {
|
|
68
|
+
message
|
|
69
|
+
}
|
|
70
|
+
...on UnauthenticatedError {
|
|
71
|
+
message
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION = """
|
|
79
|
+
mutation CreateCloudChatFromRepository($repositoryId: String!, $provider: String) {
|
|
80
|
+
createCloudChat(repositoryId: $repositoryId, provider: $provider) {
|
|
81
|
+
__typename
|
|
82
|
+
...on Chat {
|
|
83
|
+
chatUuid
|
|
84
|
+
}
|
|
85
|
+
...on UnauthenticatedError {
|
|
86
|
+
message
|
|
87
|
+
}
|
|
88
|
+
...on ChatNotFoundError {
|
|
89
|
+
message
|
|
90
|
+
}
|
|
91
|
+
...on CloudConfigNotFoundError {
|
|
92
|
+
message
|
|
93
|
+
}
|
|
94
|
+
...on GithubConfigNotFoundError {
|
|
95
|
+
message
|
|
96
|
+
}
|
|
97
|
+
...on CloudSessionError {
|
|
98
|
+
message
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
ENABLE_CLOUD_REPOSITORY_MUTATION = """
|
|
106
|
+
mutation EnableCloudRepository($orgName: String!, $repoName: String!) {
|
|
107
|
+
enableCloudRepository(orgName: $orgName, repoName: $repoName) {
|
|
108
|
+
__typename
|
|
109
|
+
...on ContainerImage {
|
|
110
|
+
buildRef
|
|
111
|
+
createdAt
|
|
112
|
+
updatedAt
|
|
113
|
+
}
|
|
114
|
+
...on UnauthenticatedError {
|
|
115
|
+
message
|
|
116
|
+
}
|
|
117
|
+
...on CloudConfigNotFoundError {
|
|
118
|
+
message
|
|
119
|
+
}
|
|
120
|
+
...on GithubConfigNotFoundError {
|
|
121
|
+
message
|
|
122
|
+
}
|
|
123
|
+
...on CloudSessionError {
|
|
124
|
+
message
|
|
125
|
+
}
|
|
126
|
+
...on Error {
|
|
127
|
+
message
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
REBUILD_CLOUD_REPOSITORY_MUTATION = """
|
|
135
|
+
mutation RebuildCloudRepository($orgName: String!, $repoName: String!) {
|
|
136
|
+
rebuildCloudRepository(orgName: $orgName, repoName: $repoName) {
|
|
137
|
+
__typename
|
|
138
|
+
...on ContainerImage {
|
|
139
|
+
buildRef
|
|
140
|
+
createdAt
|
|
141
|
+
updatedAt
|
|
142
|
+
}
|
|
143
|
+
...on UnauthenticatedError {
|
|
144
|
+
message
|
|
145
|
+
}
|
|
146
|
+
...on CloudConfigNotFoundError {
|
|
147
|
+
message
|
|
148
|
+
}
|
|
149
|
+
...on GithubConfigNotFoundError {
|
|
150
|
+
message
|
|
151
|
+
}
|
|
152
|
+
...on CloudSessionError {
|
|
153
|
+
message
|
|
154
|
+
}
|
|
155
|
+
...on Error {
|
|
156
|
+
message
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
"""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
GITHUB_REPOSITORIES_QUERY: str = """
|
|
2
|
+
query GithubRepositories {
|
|
3
|
+
githubRepositories {
|
|
4
|
+
__typename
|
|
5
|
+
... on GithubRepositories {
|
|
6
|
+
repositories {
|
|
7
|
+
id
|
|
8
|
+
githubOrgName
|
|
9
|
+
githubRepoName
|
|
10
|
+
baseHost
|
|
11
|
+
containerImageId
|
|
12
|
+
createdAt
|
|
13
|
+
updatedAt
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
... on Error {
|
|
17
|
+
message
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
EVENTS_FOR_CHAT_QUERY: str = """query EventsForChat($chatUuid: UUID!) {
|
|
25
|
+
eventsForChat(chatUuid: $chatUuid) {
|
|
26
|
+
... on EventHistory {
|
|
27
|
+
events {
|
|
28
|
+
... on UserEvent {
|
|
29
|
+
uuid
|
|
30
|
+
parentUuid
|
|
31
|
+
chatId
|
|
32
|
+
isSidechain
|
|
33
|
+
version
|
|
34
|
+
createdAt
|
|
35
|
+
sidechainRootUuid
|
|
36
|
+
synthetic
|
|
37
|
+
messageData: message {
|
|
38
|
+
... on TextMessage {
|
|
39
|
+
text
|
|
40
|
+
}
|
|
41
|
+
... on ToolCallMessage {
|
|
42
|
+
messageId
|
|
43
|
+
toolUseId
|
|
44
|
+
toolName
|
|
45
|
+
toolInput {
|
|
46
|
+
... on BashToolInput {
|
|
47
|
+
command
|
|
48
|
+
}
|
|
49
|
+
... on ReadToolInput {
|
|
50
|
+
filePath
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
... on ToolResultMessage {
|
|
55
|
+
messageId
|
|
56
|
+
toolUseId
|
|
57
|
+
text
|
|
58
|
+
resultData {
|
|
59
|
+
... on BashToolResult {
|
|
60
|
+
shellOutput
|
|
61
|
+
exitCode
|
|
62
|
+
}
|
|
63
|
+
... on ReadToolResult {
|
|
64
|
+
content
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
... on AssistantEvent {
|
|
71
|
+
uuid
|
|
72
|
+
parentUuid
|
|
73
|
+
chatId
|
|
74
|
+
isSidechain
|
|
75
|
+
version
|
|
76
|
+
createdAt
|
|
77
|
+
sidechainRootUuid
|
|
78
|
+
synthetic
|
|
79
|
+
messageData: message {
|
|
80
|
+
... on TextMessage {
|
|
81
|
+
text
|
|
82
|
+
}
|
|
83
|
+
... on ToolCallMessage {
|
|
84
|
+
messageId
|
|
85
|
+
toolUseId
|
|
86
|
+
toolName
|
|
87
|
+
toolInput {
|
|
88
|
+
... on BashToolInput {
|
|
89
|
+
command
|
|
90
|
+
}
|
|
91
|
+
... on ReadToolInput {
|
|
92
|
+
filePath
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
... on SystemEvent {
|
|
99
|
+
uuid
|
|
100
|
+
parentUuid
|
|
101
|
+
chatId
|
|
102
|
+
isSidechain
|
|
103
|
+
version
|
|
104
|
+
createdAt
|
|
105
|
+
sidechainRootUuid
|
|
106
|
+
messageData: message {
|
|
107
|
+
... on ToolCallMessage {
|
|
108
|
+
messageId
|
|
109
|
+
toolUseId
|
|
110
|
+
toolName
|
|
111
|
+
toolInput {
|
|
112
|
+
... on BashToolInput {
|
|
113
|
+
command
|
|
114
|
+
}
|
|
115
|
+
... on ReadToolInput {
|
|
116
|
+
filePath
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
... on ToolResultMessage {
|
|
121
|
+
messageId
|
|
122
|
+
toolUseId
|
|
123
|
+
text
|
|
124
|
+
resultData {
|
|
125
|
+
... on BashToolResult {
|
|
126
|
+
shellOutput
|
|
127
|
+
exitCode
|
|
128
|
+
}
|
|
129
|
+
... on ReadToolResult {
|
|
130
|
+
content
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
... on ToolExecutionStatusMessage {
|
|
135
|
+
executionStatus: status
|
|
136
|
+
}
|
|
137
|
+
... on ToolPermissionStatusMessage {
|
|
138
|
+
permissionStatus: status
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
"""
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
from pygit2.repository import Repository
|
|
6
|
+
|
|
7
|
+
from exponent.core.remote_execution.types import (
|
|
8
|
+
CreateCheckpointRequest,
|
|
9
|
+
CreateCheckpointResponse,
|
|
10
|
+
GitCommitMetadata,
|
|
11
|
+
GitDiff,
|
|
12
|
+
GitFileChange,
|
|
13
|
+
RollbackToCheckpointResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def create_checkpoint(
|
|
18
|
+
request: CreateCheckpointRequest,
|
|
19
|
+
) -> CreateCheckpointResponse:
|
|
20
|
+
repo = Repository(".")
|
|
21
|
+
head_commit = str(repo.head.target)
|
|
22
|
+
uncommitted_changes_commit = None
|
|
23
|
+
diff_versus_last_checkpoint = None
|
|
24
|
+
|
|
25
|
+
# Get metadata for head commit - fetch each field separately
|
|
26
|
+
author_name = (
|
|
27
|
+
subprocess.run(
|
|
28
|
+
["git", "log", "--format=%aN", "-1", head_commit],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
check=True,
|
|
32
|
+
).stdout.strip()
|
|
33
|
+
or "unknown"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
author_email = (
|
|
37
|
+
subprocess.run(
|
|
38
|
+
["git", "log", "--format=%aE", "-1", head_commit],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
check=True,
|
|
42
|
+
).stdout.strip()
|
|
43
|
+
or "unknown@unknown"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
author_date = subprocess.run(
|
|
47
|
+
["git", "log", "--format=%ai", "-1", head_commit],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
check=True,
|
|
51
|
+
).stdout.strip()
|
|
52
|
+
|
|
53
|
+
commit_date = subprocess.run(
|
|
54
|
+
["git", "log", "--format=%ci", "-1", head_commit],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
check=True,
|
|
58
|
+
).stdout.strip()
|
|
59
|
+
|
|
60
|
+
commit_message = subprocess.run(
|
|
61
|
+
["git", "log", "--format=%B", "-1", head_commit],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
check=True,
|
|
65
|
+
).stdout.strip()
|
|
66
|
+
|
|
67
|
+
# Get current branch
|
|
68
|
+
branch_result = subprocess.run(
|
|
69
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
)
|
|
74
|
+
branch = branch_result.stdout.strip()
|
|
75
|
+
|
|
76
|
+
head_metadata = GitCommitMetadata(
|
|
77
|
+
author_name=author_name,
|
|
78
|
+
author_email=author_email,
|
|
79
|
+
author_date=author_date,
|
|
80
|
+
commit_date=commit_date,
|
|
81
|
+
branch=branch,
|
|
82
|
+
commit_message=commit_message,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if repo.status(): # working dir is dirty
|
|
86
|
+
with tempfile.NamedTemporaryFile(prefix="git_index_") as tmp:
|
|
87
|
+
tmp_index_path = tmp.name
|
|
88
|
+
|
|
89
|
+
# Set up environment with temporary index
|
|
90
|
+
env = os.environ.copy()
|
|
91
|
+
env["GIT_INDEX_FILE"] = tmp_index_path
|
|
92
|
+
|
|
93
|
+
# Initialize temporary index from HEAD
|
|
94
|
+
subprocess.run(["git", "read-tree", head_commit], env=env, check=True)
|
|
95
|
+
|
|
96
|
+
# Add all files (including untracked) to temporary index
|
|
97
|
+
subprocess.run(["git", "add", "-A"], env=env, check=True)
|
|
98
|
+
|
|
99
|
+
# Write tree object from our temporary index
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["git", "write-tree"],
|
|
102
|
+
env=env,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=True,
|
|
106
|
+
)
|
|
107
|
+
tree_hash = result.stdout.strip()
|
|
108
|
+
|
|
109
|
+
if not tree_hash:
|
|
110
|
+
raise ValueError("Failed to create tree object")
|
|
111
|
+
|
|
112
|
+
# Create commit object from the tree with HEAD as parent
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
[
|
|
115
|
+
"git",
|
|
116
|
+
"commit-tree",
|
|
117
|
+
tree_hash,
|
|
118
|
+
"-p",
|
|
119
|
+
str(head_commit),
|
|
120
|
+
"-m",
|
|
121
|
+
"Checkpoint commit",
|
|
122
|
+
],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
check=True,
|
|
126
|
+
)
|
|
127
|
+
uncommitted_changes_commit = result.stdout.strip()
|
|
128
|
+
|
|
129
|
+
if not uncommitted_changes_commit:
|
|
130
|
+
raise ValueError("Failed to create checkpoint commit")
|
|
131
|
+
|
|
132
|
+
if request.last_checkpoint_head_commit:
|
|
133
|
+
last_checkpoint_commit = (
|
|
134
|
+
request.last_checkpoint_uncommitted_changes_commit
|
|
135
|
+
or request.last_checkpoint_head_commit
|
|
136
|
+
)
|
|
137
|
+
current_commit = uncommitted_changes_commit or head_commit
|
|
138
|
+
|
|
139
|
+
diff_versus_last_checkpoint = _parse_git_diff_tree(
|
|
140
|
+
last_checkpoint_commit,
|
|
141
|
+
current_commit,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return CreateCheckpointResponse(
|
|
145
|
+
correlation_id=request.correlation_id,
|
|
146
|
+
head_commit_hash=head_commit,
|
|
147
|
+
head_commit_metadata=head_metadata,
|
|
148
|
+
uncommitted_changes_commit_hash=uncommitted_changes_commit,
|
|
149
|
+
diff_versus_last_checkpoint=diff_versus_last_checkpoint,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def rollback_to_checkpoint(
|
|
154
|
+
correlation_id: str,
|
|
155
|
+
head_commit: str,
|
|
156
|
+
checkpoint_commit: str | None,
|
|
157
|
+
) -> RollbackToCheckpointResponse:
|
|
158
|
+
# Clean working directory (including untracked files) before any operations
|
|
159
|
+
subprocess.run(
|
|
160
|
+
["git", "clean", "-fd"], check=True
|
|
161
|
+
) # Remove untracked files and directories
|
|
162
|
+
subprocess.run(
|
|
163
|
+
["git", "reset", "--hard"], check=True
|
|
164
|
+
) # Remove staged/unstaged changes
|
|
165
|
+
|
|
166
|
+
# Now reset HEAD to the original commit state
|
|
167
|
+
subprocess.run(["git", "reset", "--hard", head_commit], check=True)
|
|
168
|
+
|
|
169
|
+
if checkpoint_commit:
|
|
170
|
+
# Cherry-pick the checkpoint commit to restore all changes
|
|
171
|
+
subprocess.run(
|
|
172
|
+
["git", "cherry-pick", "--no-commit", checkpoint_commit], check=True
|
|
173
|
+
)
|
|
174
|
+
subprocess.run(["git", "reset"], check=True)
|
|
175
|
+
|
|
176
|
+
return RollbackToCheckpointResponse(
|
|
177
|
+
correlation_id=correlation_id,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _parse_git_diff_tree(
|
|
182
|
+
from_commit: str, to_commit: str, max_files: int = 50
|
|
183
|
+
) -> GitDiff:
|
|
184
|
+
"""Parse git diff-tree output into a GitDiff object.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
from_commit: Starting commit hash
|
|
188
|
+
to_commit: Ending commit hash
|
|
189
|
+
max_files: Maximum number of files to include in the diff
|
|
190
|
+
"""
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
["git", "diff-tree", "--numstat", from_commit, to_commit],
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
check=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
files = []
|
|
199
|
+
for line in result.stdout.splitlines():
|
|
200
|
+
if not line.strip():
|
|
201
|
+
continue
|
|
202
|
+
added, deleted, path = line.split("\t")
|
|
203
|
+
files.append(
|
|
204
|
+
GitFileChange(path=path, lines_added=int(added), lines_deleted=int(deleted))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
total_files = len(files)
|
|
208
|
+
truncated = total_files > max_files
|
|
209
|
+
if truncated:
|
|
210
|
+
files = files[:max_files]
|
|
211
|
+
|
|
212
|
+
return GitDiff(files=files, truncated=truncated, total_files=total_files)
|