33const fs = require ( 'fs' ) ;
44const path = require ( 'path' ) ;
55
6+ // Configuration constants
7+ const MAX_TOKENS_PER_REQUEST = 80000 ; // Conservative limit for Gemini 2.5 Flash
8+ const CHARS_PER_TOKEN = 4 ; // Rough estimation
9+ //const MAX_CHARS_PER_CHUNK = MAX_TOKENS_PER_REQUEST * CHARS_PER_TOKEN;
10+ const MAX_CHUNKS = 3 ; // Limit to prevent excessive API calls
11+
12+ /**
13+ * Estimate token count for text (rough approximation)
14+ */
15+ function estimateTokens ( text ) {
16+ return Math . ceil ( text . length / CHARS_PER_TOKEN ) ;
17+ }
18+
19+
20+ function sleep ( ms ) {
21+ return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
22+ }
23+
24+
25+ function splitStringByTokens ( str , maxTokens ) {
26+ console . error ( 'splitStringByTokens' ) ;
27+ const words = str . split ( ' ' ) ;
28+ const result = [ ] ;
29+ let currentLine = '' ;
30+
31+ for ( const word of words ) {
32+ if ( estimateTokens ( currentLine + word ) <= maxTokens ) {
33+ currentLine += ( currentLine ? ' ' : '' ) + word ;
34+ } else {
35+ if ( currentLine ) result . push ( currentLine ) ;
36+ currentLine = word ;
37+ }
38+ }
39+
40+ if ( currentLine ) result . push ( currentLine ) ;
41+
42+ return result ;
43+ }
44+
45+
46+ /**
47+ * Split diff into chunks by file boundaries
48+ */
49+ function chunkDiffByFiles ( diffContent ) {
50+ console . error ( 'chunkDiffByFiles' ) ;
51+ const fileChunks = [ ] ;
52+ const lines = diffContent . split ( '\n' ) ;
53+ let currentChunk = '' ;
54+ let currentFile = '' ;
55+ let tokenCount = 0 ;
56+
57+ for ( const line of lines ) {
58+ // Check if this is a new file header
59+ //console.error(`Line is estimated at ${estimateTokens(line)} tokens`);
60+ tokenCount += estimateTokens ( line ) ;
61+ //console.error(`Total tokens for this chunk is ${tokenCount}`);
62+ if ( line . startsWith ( 'diff --git' ) || line . startsWith ( '+++' ) || line . startsWith ( '---' ) ) {
63+ // If we have content and it's getting large, save current chunk
64+ if ( currentChunk && tokenCount > MAX_TOKENS_PER_REQUEST ) {
65+ fileChunks . push ( {
66+ content : currentChunk . trim ( ) ,
67+ file : currentFile ,
68+ type : 'file-chunk'
69+ } ) ;
70+ currentChunk = '' ;
71+ tokenCount = 0 ;
72+ }
73+
74+ // Start new chunk
75+ currentChunk = line + '\n' ;
76+
77+
78+ // Extract filename for reference
79+ if ( line . startsWith ( '+++' ) ) {
80+ currentFile = line . replace ( '+++ b/' , '' ) . replace ( '+++ a/' , '' ) ;
81+ }
82+ if ( tokenCount > MAX_TOKENS_PER_REQUEST ) {
83+ const split_chunk = splitStringByTokens ( currentChunk , MAX_TOKENS_PER_REQUEST ) ;
84+ currentChunk = split_chunk [ split_chunk . length - 1 ] ;
85+ for ( let i = 0 ; i < split_chunk . length - 1 ; i ++ ) {
86+ fileChunks . push ( {
87+ content : split_chunk [ i ] . trim ( ) ,
88+ file : currentFile ,
89+ type : 'file-chunk'
90+ } ) ;
91+ }
92+ }
93+ } else {
94+ currentChunk += line + '\n' ;
95+ }
96+ }
97+
98+ // Add the last chunk
99+ if ( currentChunk . trim ( ) ) {
100+ fileChunks . push ( {
101+ content : currentChunk . trim ( ) ,
102+ file : currentFile ,
103+ type : 'file-chunk'
104+ } ) ;
105+ }
106+
107+ return fileChunks ;
108+ }
109+
110+ /**
111+ * Create a summary prompt for extremely large diffs
112+ */
113+ function createSummaryPrompt ( diffContent ) {
114+ return `Analyze this git diff and provide a high-level summary. Focus on:
115+ 1. What types of files were changed (e.g., source code, tests, config, docs)
116+ 2. The overall scope of changes (e.g., new feature, bug fix, refactor)
117+ 3. Any major architectural changes or new dependencies
118+
119+ Keep the summary to 2-3 sentences maximum.
120+
121+ Git diff:
122+ ${ diffContent } `;
123+ }
124+
125+ /**
126+ * Create the main PR description prompt
127+ */
128+ function createPRPrompt ( diffContent ) {
129+ return `Write a release note summary with the following sections:
130+
131+ ## Infrastructure Changes
132+ Highlight changes to AWS resources, IaC (CloudFormation/SAM templates), Lambda functions, databases, and deployment configurations.
133+
134+ ## Security Concerns
135+ Identify any security-related changes, authentication/authorization updates, data access modifications, or potential vulnerabilities.
136+
137+ ## Performance Implications
138+ Assess any changes that could impact system performance, database queries, API response times, or resource consumption.
139+
140+ ## New Features
141+ Describe new functionality, enhancements, or capabilities being introduced.
142+
143+ ## Student Access Risk Analysis
144+ Evaluate the risk that these changes could cause students to lose access to their course materials. Assess changes to:
145+
146+ - Term ID handling and relationships
147+ - Adoption, section, product, or term product relationships
148+ - Data export or deduplication logic
149+ - Database schema affecting terms, sections, or student access
150+ - Section Channel Offer (SCO) activation/deactivation logic
151+ - Query logic for student materials or permissions
152+
153+ For each risk identified, provide: Risk Level (NONE/LOW/MEDIUM/HIGH/CRITICAL), specific changes, risk description, and recommended verification steps.
154+
155+ If no student access risks are detected, state: "Risk Level: NONE - No changes affect student material access."
156+
157+ Print only the report and ask no questions.
158+
159+ ${ diffContent } `;
160+ }
161+
162+ /**
163+ * Call Gemini API with the given prompt
164+ */
165+ async function callGeminiAPI ( prompt , apiKey ) {
166+ console . error ( `Sending prompt with an estimated ${ estimateTokens ( prompt ) } tokens` ) ;
167+ const response = await fetch ( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ apiKey } ` , {
168+ method : 'POST' ,
169+ headers : { 'Content-Type' : 'application/json' } ,
170+ body : JSON . stringify ( {
171+ contents : [ {
172+ parts : [ {
173+ text : prompt
174+ } ]
175+ } ] ,
176+ generationConfig : {
177+ temperature : 0.7 ,
178+ topK : 40 ,
179+ topP : 0.95 ,
180+ maxOutputTokens : 8192 ,
181+ }
182+ } )
183+ } ) ;
184+
185+ if ( ! response . ok ) {
186+ const errorText = await response . text ( ) ;
187+ throw new Error ( `Gemini API request failed with status ${ response . status } : ${ errorText } ` ) ;
188+ }
189+
190+ const json = await response . json ( ) ;
191+
192+ if ( ! json . candidates || ! json . candidates [ 0 ] ) {
193+ throw new Error ( 'Invalid response from Gemini API' ) ;
194+ }
195+
196+ if ( ! json . candidates [ 0 ] . content || ! json . candidates [ 0 ] . content . parts || ! json . candidates [ 0 ] . content . parts [ 0 ] || ! json . candidates [ 0 ] . content . parts [ 0 ] . text ) {
197+ throw new Error ( 'Invalid response structure from Gemini API - missing content' ) ;
198+ }
199+
200+ return json . candidates [ 0 ] . content . parts [ 0 ] . text ;
201+ }
202+
203+ /**
204+ * Process diff chunks and combine results
205+ */
206+ async function processChunks ( chunks , apiKey ) {
207+ console . error ( 'processchunks' ) ;
208+ if ( chunks . length === 1 ) {
209+ // Single chunk, process normally
210+ return await callGeminiAPI ( createPRPrompt ( chunks [ 0 ] . content ) , apiKey ) ;
211+ }
212+
213+ // Multiple chunks - process each and combine
214+ const chunkResults = [ ] ;
215+
216+ for ( let i = 0 ; i < Math . min ( chunks . length , MAX_CHUNKS ) ; i ++ ) {
217+ const chunk = chunks [ i ] ;
218+ if ( i > 0 ) {
219+ // sleep for 3 seconds
220+ sleep ( 5 * 1000 ) ;
221+ }
222+ console . error ( `Processing chunk ${ i + 1 } /${ Math . min ( chunks . length , MAX_CHUNKS ) } (${ chunk . file || 'unknown file' } )` ) ;
223+
224+ try {
225+ const result = await callGeminiAPI ( createPRPrompt ( chunk . content ) , apiKey ) ;
226+ chunkResults . push ( {
227+ file : chunk . file ,
228+ result : result
229+ } ) ;
230+ } catch ( error ) {
231+ console . error ( `Warning: Failed to process chunk ${ i + 1 } : ${ error . message } ` ) ;
232+ // Continue with other chunks
233+ }
234+ }
235+
236+ if ( chunkResults . length === 0 ) {
237+ throw new Error ( 'Failed to process any chunks' ) ;
238+ }
239+ sleep ( 5 * 1000 ) ;
240+ // Combine results from multiple chunks
241+ const combinedPrompt = `Combine these pull request descriptions into a single, coherent PR description. Use the same format:
242+
243+ ${ chunkResults . map ( ( chunk , index ) => `## Chunk ${ index + 1 } (${ chunk . file } ):
244+ ${ chunk . result } `) . join ( '\n\n' ) }
245+
246+ Create a unified description that captures the overall changes across all files.` ;
247+
248+ return await callGeminiAPI ( combinedPrompt , apiKey ) ;
249+ }
250+
6251( async ( ) => {
7252 const [ , , diffFile ] = process . argv ;
8253 if ( ! diffFile ) {
@@ -21,80 +266,28 @@ const path = require('path');
21266 process . exit ( 1 ) ;
22267 }
23268
24- // Create prompt for PR description generation
25- const promptTemplate = `Write a concise pull request description based on the git diff. Use this exact format:
26-
27- ## Description
28- Brief summary of changes (1-2 sentences max).
29-
30- ## Changes
31- - [ ] Key change 1
32- - [ ] Key change 2
33- - [ ] Key change 3 (max 5 items)
34-
35- ## Verification
36- - [ ] Test step 1
37- - [ ] Test step 2
38- - [ ] Test step 3 (max 3 items)
39-
40- Keep it concise and focused on the most important changes.` ;
41-
42269 const diffContent = fs . readFileSync ( diffFile , 'utf8' ) ;
43- const combinedPrompt = `${ promptTemplate } \n\nHere is the git diff:\n\n${ diffContent } ` ;
270+ const estimatedTokens = estimateTokens ( diffContent ) ;
271+
272+ console . error ( `Diff size: ${ diffContent . length } characters (~${ estimatedTokens } tokens)` ) ;
44273
45274 try {
46- const response = await fetch ( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ apiKey } ` , {
47- method : 'POST' ,
48- headers : { 'Content-Type' : 'application/json' } ,
49- body : JSON . stringify ( {
50- contents : [ {
51- parts : [ {
52- text : combinedPrompt
53- } ]
54- } ] ,
55- generationConfig : {
56- temperature : 0.7 ,
57- topK : 40 ,
58- topP : 0.95 ,
59- maxOutputTokens : 8192 ,
60- }
61- } )
62- } ) ;
63-
64- if ( ! response . ok ) {
65- const errorText = await response . text ( ) ;
66- console . error ( `Error: Gemini API request failed with status ${ response . status } ` ) ;
67- console . error ( `Response: ${ errorText } ` ) ;
68- process . exit ( 1 ) ;
69- }
70-
71- const json = await response . json ( ) ;
72-
73- if ( ! json . candidates || ! json . candidates [ 0 ] ) {
74- console . error ( 'Error: Invalid response from Gemini API' ) ;
75- console . error ( JSON . stringify ( json , null , 2 ) ) ;
76- process . exit ( 1 ) ;
77- }
78-
79- // Check if response was truncated due to max tokens
80- if ( json . candidates [ 0 ] . finishReason === 'MAX_TOKENS' ) {
81- console . error ( 'Warning: Response was truncated due to token limit. Consider reducing diff size or using more specific ignore-files.' ) ;
82- // Continue processing the partial response
83- }
84-
85- if ( ! json . candidates [ 0 ] . content ) {
86- console . error ( 'Error: No content in API response' ) ;
87- console . error ( JSON . stringify ( json , null , 2 ) ) ;
88- process . exit ( 1 ) ;
89- }
275+ let result ;
90276
91- if ( ! json . candidates [ 0 ] . content . parts || ! json . candidates [ 0 ] . content . parts [ 0 ] || ! json . candidates [ 0 ] . content . parts [ 0 ] . text ) {
92- console . error ( 'Error: Invalid response structure from Gemini API - missing parts or text' ) ;
93- console . error ( JSON . stringify ( json , null , 2 ) ) ;
94- process . exit ( 1 ) ;
277+ if ( estimatedTokens > MAX_TOKENS_PER_REQUEST ) {
278+ console . error ( 'Large diff detected, using chunking strategy...' ) ;
279+
280+ const chunks = chunkDiffByFiles ( diffContent ) ;
281+ console . error ( `Split diff into ${ chunks . length } chunks` ) ;
282+ if ( chunks . length > MAX_CHUNKS ) {
283+ console . error ( `Warning: Too many chunks (${ chunks . length } ), processing first ${ MAX_CHUNKS } chunks only` ) ;
284+ }
285+ result = await processChunks ( chunks , apiKey ) ;
286+ } else {
287+ // Small diff, process normally
288+ result = await callGeminiAPI ( createPRPrompt ( diffContent ) , apiKey ) ;
95289 }
96290
97- const result = json . candidates [ 0 ] . content . parts [ 0 ] . text ;
98291 process . stdout . write ( result ) ;
99292 } catch ( error ) {
100293 console . error ( `Error: Failed to generate pull request description: ${ error . message } ` ) ;
0 commit comments