@@ -2552,6 +2552,273 @@ def get_user_resolved_threads(self, username: str, project_id: Optional[str] = N
25522552 }
25532553 }
25542554
2555+ @retry_on_error ()
2556+ def get_repository_tree (self , project_id : str , path : str = "" , ref : Optional [str ] = None ,
2557+ recursive : bool = False ) -> Dict [str , Any ]:
2558+ """Get repository tree (list of files and directories).
2559+
2560+ Args:
2561+ project_id: The ID or path of the project
2562+ path: The path inside repository to list
2563+ ref: The name of a repository branch or tag
2564+ recursive: Boolean value to get a recursive tree
2565+
2566+ Returns:
2567+ Dict containing repository tree items
2568+ """
2569+ try :
2570+ project = self .gl .projects .get (project_id )
2571+ kwargs = {"path" : path , "recursive" : recursive }
2572+ if ref :
2573+ kwargs ["ref" ] = ref
2574+
2575+ tree = project .repository_tree (** kwargs )
2576+
2577+ return {
2578+ "tree" : [
2579+ {
2580+ "id" : item .get ("id" ),
2581+ "name" : item .get ("name" ),
2582+ "type" : item .get ("type" ), # "tree" for directory, "blob" for file
2583+ "path" : item .get ("path" ),
2584+ "mode" : item .get ("mode" ),
2585+ }
2586+ for item in tree
2587+ ],
2588+ "project_id" : project_id ,
2589+ "path" : path ,
2590+ "ref" : ref ,
2591+ }
2592+ except gitlab .exceptions .GitlabGetError as e :
2593+ return {"error" : f"Failed to get repository tree: { str (e )} " }
2594+
2595+ @retry_on_error ()
2596+ def search_in_project (self , project_id : str , scope : str , search : str ,
2597+ per_page : int = DEFAULT_PAGE_SIZE , page : int = 1 ) -> Dict [str , Any ]:
2598+ """Search within a project.
2599+
2600+ Args:
2601+ project_id: The ID or path of the project
2602+ scope: Scope of search (blobs, issues, merge_requests, milestones, notes, wiki_blobs, commits)
2603+ search: Search query
2604+ per_page: Number of items per page
2605+ page: Page number
2606+
2607+ Returns:
2608+ Dict containing search results
2609+ """
2610+ try :
2611+ project = self .gl .projects .get (project_id )
2612+ results = project .search (scope , search , get_all = False , per_page = per_page , page = page )
2613+
2614+ # Format results based on scope
2615+ formatted_results = []
2616+ for item in results :
2617+ if scope == "blobs" :
2618+ formatted_results .append ({
2619+ "basename" : getattr (item , "basename" , None ),
2620+ "data" : getattr (item , "data" , None ),
2621+ "path" : getattr (item , "path" , None ),
2622+ "filename" : getattr (item , "filename" , None ),
2623+ "id" : getattr (item , "id" , None ),
2624+ "ref" : getattr (item , "ref" , None ),
2625+ "startline" : getattr (item , "startline" , None ),
2626+ "project_id" : getattr (item , "project_id" , None ),
2627+ })
2628+ elif scope == "commits" :
2629+ formatted_results .append ({
2630+ "id" : getattr (item , "id" , None ),
2631+ "short_id" : getattr (item , "short_id" , None ),
2632+ "title" : getattr (item , "title" , None ),
2633+ "message" : getattr (item , "message" , None ),
2634+ "author_name" : getattr (item , "author_name" , None ),
2635+ "created_at" : getattr (item , "created_at" , None ),
2636+ })
2637+ elif scope in ["issues" , "merge_requests" ]:
2638+ formatted_results .append ({
2639+ "id" : getattr (item , "id" , None ),
2640+ "iid" : getattr (item , "iid" , None ),
2641+ "title" : getattr (item , "title" , None ),
2642+ "description" : getattr (item , "description" , None ),
2643+ "state" : getattr (item , "state" , None ),
2644+ "created_at" : getattr (item , "created_at" , None ),
2645+ "updated_at" : getattr (item , "updated_at" , None ),
2646+ "web_url" : getattr (item , "web_url" , None ),
2647+ })
2648+ else :
2649+ # Generic formatting for other scopes
2650+ formatted_results .append ({
2651+ k : getattr (item , k , None )
2652+ for k in dir (item )
2653+ if not k .startswith ('_' )
2654+ })
2655+
2656+ pagination = {
2657+ "page" : page ,
2658+ "per_page" : per_page ,
2659+ "total" : getattr (results , "total" , None ),
2660+ "total_pages" : getattr (results , "total_pages" , None ),
2661+ "next_page" : getattr (results , "next_page" , None ),
2662+ "prev_page" : getattr (results , "prev_page" , None ),
2663+ }
2664+
2665+ return {
2666+ "results" : formatted_results ,
2667+ "pagination" : pagination ,
2668+ "scope" : scope ,
2669+ "search" : search ,
2670+ "project_id" : project_id ,
2671+ }
2672+ except gitlab .exceptions .GitlabGetError as e :
2673+ return {"error" : f"Search failed: { str (e )} " }
2674+ except Exception as e :
2675+ return {"error" : f"Failed to search in project: { str (e )} " }
2676+
2677+ @retry_on_error ()
2678+ def summarize_merge_request (self , project_id : str , mr_iid : int , max_length : int = 500 ) -> Dict [str , Any ]:
2679+ """Generate an AI-friendly summary of a merge request.
2680+
2681+ This method retrieves MR details, changes, and discussions, then formats them
2682+ into a concise summary suitable for LLM context.
2683+
2684+ Args:
2685+ project_id: The ID or path of the project
2686+ mr_iid: The IID of the merge request
2687+ max_length: Maximum length for description/discussion summary
2688+
2689+ Returns:
2690+ Dict containing structured MR summary with key information
2691+ """
2692+ try :
2693+ project = self .gl .projects .get (project_id )
2694+ mr = project .mergerequests .get (mr_iid )
2695+
2696+ # Get MR changes
2697+ changes = mr .changes ()
2698+
2699+ # Get discussions
2700+ discussions = mr .discussions .list (get_all = True )
2701+
2702+ # Summarize files changed
2703+ files_changed = []
2704+ for change in changes .get ("changes" , []):
2705+ files_changed .append ({
2706+ "path" : change .get ("new_path" ),
2707+ "additions" : change .get ("diff" , "" ).count ("\n +" ),
2708+ "deletions" : change .get ("diff" , "" ).count ("\n -" ),
2709+ })
2710+
2711+ # Summarize discussions
2712+ discussion_summary = []
2713+ for discussion in discussions [:5 ]: # Limit to first 5 discussions
2714+ notes = discussion .attributes .get ("notes" , [])
2715+ if notes :
2716+ first_note = notes [0 ]
2717+ discussion_summary .append ({
2718+ "author" : first_note .get ("author" , {}).get ("username" ),
2719+ "created_at" : first_note .get ("created_at" ),
2720+ "resolved" : discussion .attributes .get ("resolved" , False ),
2721+ "note_count" : len (notes ),
2722+ })
2723+
2724+ # Create summary
2725+ description = mr .description or ""
2726+ if len (description ) > max_length :
2727+ description = description [:max_length ] + "..."
2728+
2729+ return {
2730+ "mr_iid" : mr_iid ,
2731+ "title" : mr .title ,
2732+ "state" : mr .state ,
2733+ "author" : mr .author .get ("username" ),
2734+ "created_at" : mr .created_at ,
2735+ "updated_at" : mr .updated_at ,
2736+ "source_branch" : mr .source_branch ,
2737+ "target_branch" : mr .target_branch ,
2738+ "description_summary" : description ,
2739+ "files_changed_count" : len (files_changed ),
2740+ "files_changed_sample" : files_changed [:10 ], # First 10 files
2741+ "additions_total" : sum (f ["additions" ] for f in files_changed ),
2742+ "deletions_total" : sum (f ["deletions" ] for f in files_changed ),
2743+ "discussion_count" : len (discussions ),
2744+ "discussions_summary" : discussion_summary ,
2745+ "merge_status" : mr .merge_status ,
2746+ "has_conflicts" : mr .has_conflicts ,
2747+ "labels" : mr .labels ,
2748+ "web_url" : mr .web_url ,
2749+ }
2750+ except gitlab .exceptions .GitlabGetError as e :
2751+ return {"error" : f"Failed to get merge request: { str (e )} " }
2752+
2753+ @retry_on_error ()
2754+ def batch_operations (self , project_id : str , operations : List [Dict [str , Any ]],
2755+ stop_on_error : bool = True ) -> Dict [str , Any ]:
2756+ """Execute multiple operations in batch.
2757+
2758+ Args:
2759+ project_id: The ID or path of the project
2760+ operations: List of operations to execute
2761+ stop_on_error: Whether to stop on first error
2762+
2763+ Returns:
2764+ Dict containing results of all operations
2765+ """
2766+ results = []
2767+
2768+ for i , operation in enumerate (operations ):
2769+ try :
2770+ op_type = operation .get ("type" )
2771+ op_params = operation .get ("params" , {})
2772+
2773+ # Add project_id to params if not present
2774+ if "project_id" not in op_params :
2775+ op_params ["project_id" ] = project_id
2776+
2777+ # Execute operation based on type
2778+ if op_type == "get_issue" :
2779+ result = self .get_issue (** op_params )
2780+ elif op_type == "get_merge_request" :
2781+ result = self .get_merge_request (** op_params )
2782+ elif op_type == "list_issues" :
2783+ result = self .list_issues (** op_params )
2784+ elif op_type == "list_merge_requests" :
2785+ result = self .list_merge_requests (** op_params )
2786+ elif op_type == "get_file_content" :
2787+ result = self .get_file_content (** op_params )
2788+ elif op_type == "get_commits" :
2789+ result = self .get_commits (** op_params )
2790+ else :
2791+ result = {"error" : f"Unknown operation type: { op_type } " }
2792+
2793+ results .append ({
2794+ "index" : i ,
2795+ "operation" : op_type ,
2796+ "success" : "error" not in result ,
2797+ "result" : result ,
2798+ })
2799+
2800+ if stop_on_error and "error" in result :
2801+ break
2802+
2803+ except Exception as e :
2804+ error_result = {
2805+ "index" : i ,
2806+ "operation" : operation .get ("type" ),
2807+ "success" : False ,
2808+ "result" : {"error" : str (e )},
2809+ }
2810+ results .append (error_result )
2811+
2812+ if stop_on_error :
2813+ break
2814+
2815+ return {
2816+ "operations_count" : len (operations ),
2817+ "executed_count" : len (results ),
2818+ "success_count" : sum (1 for r in results if r ["success" ]),
2819+ "results" : results ,
2820+ }
2821+
25552822
25562823__all__ = ["GitLabClient" , "GitLabConfig" ]
25572824
0 commit comments