1010from codetide .agents .tide .ui .utils import run_concurrent_tasks
1111from codetide .agents .tide .ui .agent_tide_ui import AgentTideUi
1212from codetide .core .defaults import DEFAULT_ENCODING
13+ from codetide .core .logs import logger
1314from codetide .agents .tide .models import Step
1415
1516from aicore .const import STREAM_END_TOKEN , STREAM_START_TOKEN
1617from aicore .models import AuthenticationError , ModelError
1718from aicore .config import Config
1819from aicore .llm import Llm
1920
20- from typing import Optional
21+ from git_utils import commit_and_push_changes , validate_git_url
2122from chainlit .cli import run_chainlit
23+ from typing import Optional
2224from pathlib import Path
2325from ulid import ulid
2426import chainlit as cl
2527import subprocess
28+ import asyncio
2629import shutil
27- import yaml
2830import json
31+ import stat
32+ import yaml
2933import os
30- import re
3134
3235DEFAULT_SESSIONS_WORKSPACE = Path (os .getcwd ()) / "sessions"
3336
34- GIT_URL_PATTERN = re .compile (
35- r'^(?:http|https|git|ssh)://' # Protocol
36- r'(?:\S+@)?' # Optional username
37- r'([^/]+)' # Domain
38- r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path
39- )
40-
41- def validate_git_url (url ) -> None :
42- """Validate the Git repository URL using git ls-remote."""
43- if not GIT_URL_PATTERN .match (url ):
44- raise ValueError (f"Invalid Git repository URL format: { url } " )
45-
46- try :
47- result = subprocess .run (
48- ["git" , "ls-remote" , url ],
49- capture_output = True ,
50- text = True ,
51- check = True ,
52- timeout = 10 # Add timeout to prevent hanging
53- )
54- if not result .stdout .strip ():
55- raise ValueError (f"URL { url } points to an empty repository" )
56- except subprocess .TimeoutExpired :
57- raise ValueError (f"Timeout while validating URL { url } " )
58- except subprocess .CalledProcessError as e :
59- raise ValueError (f"Invalid Git repository URL: { url } . Error: { e .stderr } " ) from e
60-
6137async def validate_llm_config_hf (agent_tide_ui : AgentTideUi ):
6238 exception = True
6339 session_id = cl .user_session .get ("session_id" )
@@ -141,22 +117,35 @@ async def clone_repo(session_id):
141117
142118 while exception :
143119 try :
144- url = await cl .AskUserMessage (
120+ user_message = await cl .AskUserMessage (
145121 content = "Provide a valid github url to give AgentTide some context!"
146122 ).send ()
147- validate_git_url (url )
123+ url = user_message .get ("output" )
124+ await validate_git_url (url )
148125 exception = None
149126 except Exception as e :
150127 await cl .Message (f"Invalid url found, please provide only the url, if it is a private repo you can inlucde a PAT in the url: { e } " ).send ()
151128 exception = e
152129
153- subprocess . run (
154- [ "git" , "clone" , "--no-checkout" , url , DEFAULT_SESSIONS_WORKSPACE / session_id ],
155- check = True ,
156- capture_output = True ,
157- text = True ,
158- timeout = 300
130+ logger . info ( f"executing cmd git clone --no-checkout { url } { DEFAULT_SESSIONS_WORKSPACE / session_id } " )
131+
132+ process = await asyncio . create_subprocess_exec (
133+ "git" , "clone" , url , str ( DEFAULT_SESSIONS_WORKSPACE / session_id ) ,
134+ stdout = asyncio . subprocess . PIPE ,
135+ stderr = asyncio . subprocess . PIPE
159136 )
137+
138+ try :
139+ stdout , stderr = await asyncio .wait_for (process .communicate (), timeout = 300 )
140+ except asyncio .TimeoutError :
141+ process .kill ()
142+ await process .wait ()
143+ raise
144+
145+ if process .returncode != 0 :
146+ raise subprocess .CalledProcessError (process .returncode , ["git" , "clone" , url ], stdout , stderr )
147+
148+ logger .info (f"finished cloning to { DEFAULT_SESSIONS_WORKSPACE / session_id } " )
160149
161150
162151@cl .on_chat_start
@@ -178,10 +167,15 @@ async def empty_current_session():
178167 if os .path .exists (session_path ):
179168 shutil .rmtree (session_path )
180169
170+ def remove_readonly (func , path , _ ):
171+ """Clear the readonly bit and reattempt the removal"""
172+ os .chmod (path , stat .S_IWRITE )
173+ func (path )
174+
181175@cl .on_app_shutdown
182176async def empty_all_sessions ():
183177 if os .path .exists (DEFAULT_SESSIONS_WORKSPACE ):
184- shutil .rmtree (DEFAULT_SESSIONS_WORKSPACE )
178+ shutil .rmtree (DEFAULT_SESSIONS_WORKSPACE , onexc = remove_readonly )
185179
186180@cl .action_callback ("execute_steps" )
187181async def on_execute_steps (action :cl .Action ):
@@ -249,14 +243,19 @@ async def on_stop_steps(action :cl.Action):
249243 agent_tide_ui .current_step = None
250244 await task_list .remove ()
251245
246+ @cl .action_callback ("checkout_commit_push" )
247+ async def on_checkout_commit_push (action :cl .Action ):
248+ session_id = cl .user_session .get ("session_id" )
249+ await commit_and_push_changes (DEFAULT_SESSIONS_WORKSPACE / session_id )
250+
252251@cl .on_message
253252async def agent_loop (message : cl .Message , codeIdentifiers : Optional [list ] = None ):
254253 agent_tide_ui = await loadAgentTideUi ()
255254
256255 chat_history = cl .user_session .get ("chat_history" )
257256
258257 if message .command :
259- command_prompt = agent_tide_ui .get_command_prompt (message .command )
258+ command_prompt = await agent_tide_ui .get_command_prompt (message .command )
260259 if command_prompt :
261260 message .content = "\n \n ---\n \n " .join ([command_prompt , message .content ])
262261
@@ -314,6 +313,12 @@ async def agent_loop(message: cl.Message, codeIdentifiers: Optional[list] = None
314313 tooltip = "Next step" ,
315314 icon = "fast-forward" ,
316315 payload = {"msg_id" : msg .id }
316+ ),
317+ cl .Action (
318+ name = "checkout_commit_push" ,
319+ tooltip = "A new branch will be created and the changes made so far will be commited and pushed to the upstream repository" ,
320+ icon = "circle-fading-arrow-up" ,
321+ payload = {"msg_id" : msg .id }
317322 )
318323 ]
319324
0 commit comments