code-review-gpt
Version:
Your AI code reviewer. Improve code quality and catch bugs before you break production
115 lines (84 loc) • 32.5 kB
JavaScript
var no=Object.create;var{getPrototypeOf:to,defineProperty:C,getOwnPropertyNames:co}=Object;var so=Object.prototype.hasOwnProperty;var l=(e,r,o)=>{o=e!=null?no(to(e)):{};let i=r||!e||!e.__esModule?C(o,"default",{value:e,enumerable:!0}):o;for(let n of co(e))if(!so.call(i,n))C(i,n,{get:()=>e[n],enumerable:!0});return i};var y=(e,r)=>{for(var o in r)C(e,o,{get:r[o],enumerable:!0,configurable:!0,set:(i)=>r[o]=()=>i})};var p=(e,r)=>()=>(e&&(r=e(e=0)),r);var Ee,c;var h=p(()=>{Ee=require("tslog"),c=new Ee.Logger({prettyLogTemplate:"{{logLevelName}}\t"})});var Ae=()=>{let e=process.env.OPENAI_API_KEY,r=process.env.AZURE_OPENAI_API_KEY;if(!e&&!r)c.error("Neither OPENAI_API_KEY nor AZURE_OPENAI_API_KEY is set");return e??r??""},Ie=()=>{if(!process.env.GITHUB_TOKEN)c.error("GITHUB_TOKEN is not set");return process.env.GITHUB_TOKEN??""},F=()=>{let e=["GITHUB_SHA","BASE_SHA","GITHUB_TOKEN"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Missing environment variables: ${r.join(", ")}`),new Error("One or more GitHub environment variables are not set");return{githubSha:process.env.GITHUB_SHA??"",baseSha:process.env.BASE_SHA??"",githubToken:process.env.GITHUB_TOKEN??""}},$=()=>{let e=["CI_MERGE_REQUEST_DIFF_BASE_SHA","CI_PROJECT_ID","CI_MERGE_REQUEST_IID","CI_COMMIT_SHA","GITLAB_TOKEN","GITLAB_HOST"].filter((r)=>!process.env[r]);if(e.length>0)throw c.error(`Missing environment variables: ${e.join(", ")}`),new Error("One or more GitLab environment variables are not set. Did you set up your Gitlab access token? Refer to the README (Gitlab CI section) on how to set it up.");return{mergeRequestBaseSha:process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA??"",gitlabSha:process.env.CI_COMMIT_SHA??"",gitlabToken:process.env.GITLAB_TOKEN??"",projectId:process.env.CI_PROJECT_ID??"",mergeRequestIIdString:process.env.CI_MERGE_REQUEST_IID??"",gitlabHost:process.env.GITLAB_HOST??"https://gitlab.com"}},U=()=>{let e=["SYSTEM_PULLREQUEST_SOURCECOMMITID","BASE_SHA","API_TOKEN"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Missing environment variables: ${r.join(", ")}`),new Error("One or more Azure DevOps environment variables are not set");return{azdevSha:process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID??"",baseSha:process.env.BASE_SHA??"",azdevToken:process.env.API_TOKEN??""}};var I=p(()=>{h()});var k=()=>{};var _e,S=async(e)=>{let r=await _e.glob(e,{onlyFiles:!0});if(r.length===0)throw new Error(`No template file found for pattern: ${e}`);return r[0]};var ye=p(()=>{_e=require("tinyglobby")});var ke={};y(ke,{configure:()=>uo});var R,E,T,Fe,uo=async(e)=>{if(e.setupTarget==="github")await po();if(e.setupTarget==="gitlab")await go();if(e.setupTarget==="azdev")await wo()},$e=async()=>{return await Fe.password({message:"Please input your OpenAI API key:"})},po=async()=>{let e=await S("**/templates/github-pr.yml"),r=T.default.join(process.cwd(),".github","workflows");E.default.mkdirSync(r,{recursive:!0});let o=T.default.join(r,"code-review-gpt.yml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Created GitHub Actions workflow at: ${o}`);let i=await $e();if(!i){c.error("No API key provided. Please manually add the OPENAI_API_KEY secret to your GitHub repository.");return}try{R.execSync("gh auth status || gh auth login",{stdio:"inherit"}),R.execSync(`gh secret set OPENAI_API_KEY --body=${String(i)}`),c.info("Successfully added the OPENAI_API_KEY secret to your GitHub repository.")}catch(n){c.error("It seems that the GitHub CLI is not installed or there was an error during authentication. Don't forget to add the OPENAI_API_KEY to the repo settings/Environment/Actions/Repository Secrets manually.")}},go=async()=>{let e=await S("**/templates/gitlab-pr.yml"),r=process.cwd(),o=T.default.join(r,".gitlab-ci.yml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Created GitLab CI at: ${o}`);let i=await $e();if(!i){c.error("No API key provided. Please manually add the OPENAI_API_KEY secret to your GitLab CI/CD environment variables for your repository.");return}try{R.execSync("glab auth login",{stdio:"inherit"}),R.execSync(`glab variable set OPENAI_API_KEY ${String(i)}`),c.info(`Successfully added the OPENAI_API_KEY secret to your GitLab repository.
Please make sure you have set up your Gitlab access token before using this tool. Refer to the README (Gitlab CI section) for information on how to do this.`)}catch(n){c.error("It seems that the GitLab CLI is not installed or there was an error during authentication. Don't forget to add the OPENAI_API_KEY and the GITLAB_TOKEN to the repo's CI/CD Variables manually. Refer to the README (Gitlab CI section)for information on how to set up your access token.")}},wo=async()=>{let e=await S("**/templates/azdev-pr.yml"),r=process.cwd(),o=T.default.join(r,"code-review-gpt.yaml");E.default.writeFileSync(o,E.default.readFileSync(e,"utf8"),"utf8"),c.info(`Created Azure DevOps Pipeline at: ${o}`),c.info("Please manually add the OPENAI_API_KEY and API_TOKEN secrets as encrypted variables in the UI.")};var Ge=p(()=>{R=require("child_process"),E=l(require("fs")),T=l(require("path")),Fe=require("@inquirer/prompts");k();h();ye()});var M,ho=()=>{let e=["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI","API_TOKEN","SYSTEM_PULLREQUEST_PULLREQUESTID","BUILD_REPOSITORY_ID","SYSTEM_TEAMPROJECTID"],r=[];for(let o of e)if(!process.env[o])r.push(o);if(r.length>0)throw c.error(`Missing environment variables: ${r.join(", ")}`),new Error("One or more Azure DevOps environment variables are not set");return{serverUrl:process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI??"",azdevToken:process.env.API_TOKEN??"",pullRequestId:process.env.SYSTEM_PULLREQUEST_PULLREQUESTID??"",project:process.env.SYSTEM_TEAMPROJECTID??"",repositoryId:process.env.BUILD_REPOSITORY_ID??""}},Re=async(e,r)=>{try{let{serverUrl:o,azdevToken:i,pullRequestId:n,repositoryId:t,project:s}=ho(),u=Number(n),g=M.getPersonalAccessTokenHandler(i),w=await new M.WebApi(o,g).getGitApi(),d={comments:[{content:`${e}
---
${r}`}]};await w.createThread(d,t,u,s)}catch(o){throw c.error(`Failed to comment on PR: ${JSON.stringify(o)}`),o}};var Te=p(()=>{M=l(require("azure-devops-node-api"));h()});var ee=(e)=>{return e.map((r)=>`
${r.reasoning}
${r.suggestedChanges?`Suggested changes:
\`\`\`suggestion
${r.suggestedChanges}
\`\`\`
`:""}
<details>
<summary>View Original Code</summary>
\`\`\`code
${r.targetCodeBlock}
\`\`\`
</details>
`).join(`
`)},lo=(e)=>`
**Risk Level ${e.riskScore} - ${e.fileName}**
${ee(e.review)}
`,Ne=(e)=>`
${e.map(lo).join(`
---
`)}
`;var K,bo=(e,r)=>{let o=e.lastIndexOf(r);if(o!==-1)return e.slice(o+r.length+1);return e},re=()=>{let{githubToken:e}=F();if(!e)throw new Error("GITHUB_TOKEN is not set");return e},Oe=()=>{let e=re(),{payload:r,issue:o}=K.context;if(!r.pull_request){c.warn("Not a pull request. Skipping commenting on PR...");return}let i=K.getOctokit(e),{owner:n,repo:t,number:s}=o;return{octokit:i,owner:n,repo:t,pull_number:s}},Ue=async(e,r)=>{try{let o=`${ee(r.feedback.review)}
---
${r.signOff}`,{data:i}=await e.rest.pulls.listReviewComments({owner:r.owner,repo:r.repo,pull_number:r.pull_number}),n=bo(r.feedback.fileName,r.repo),t=i.find((s)=>s.path===n&&s.body.includes(r.signOff));if(t)await e.rest.pulls.updateReviewComment({owner:r.owner,repo:r.repo,comment_id:t.id,body:o});else await e.rest.pulls.createReviewComment({owner:r.owner,repo:r.repo,pull_number:r.pull_number,body:o,commit_id:r.commit_id,path:n,subject_type:"FILE"})}catch(o){c.error(`Failed to comment on PR for feedback: ${r.feedback.review}. Error: ${JSON.stringify(o)}`)}};var oe=p(()=>{K=require("@actions/github");I();h()});var P,B=async(e,r)=>{try{let o=re(),{payload:i,issue:n}=P.context;if(!i.pull_request){c.warn("Not a pull request. Skipping commenting on PR...");return}let t=P.getOctokit(o),{owner:s,repo:u,number:g}=n,{data:f}=await t.rest.issues.listComments({owner:s,repo:u,issue_number:g}),w=f.find((a)=>a.body?.includes(r)),d=`${e}
---
${r}`;if(w)await t.rest.issues.updateComment({owner:s,repo:u,comment_id:w.id,body:d});else await t.rest.issues.createComment({owner:s,repo:u,issue_number:g,body:d})}catch(o){throw c.error(`Failed to comment on PR: ${JSON.stringify(o)}`),o}};var ie=p(()=>{P=require("@actions/github");h();oe()});var Se=async(e,r)=>{let o=Oe();if(o){let{octokit:i,owner:n,repo:t,pull_number:s}=o,g=(await i.rest.pulls.get({owner:n,repo:t,pull_number:s})).data.head.sha;for(let f of e)await Ue(i,{feedback:f,signOff:r,owner:n,repo:t,pull_number:s,commit_id:g})}};var He=p(()=>{oe()});var Me,Y=async(e,r)=>{try{let{gitlabToken:o,projectId:i,mergeRequestIIdString:n,gitlabHost:t}=$(),s=Number.parseInt(n,10),u=new Me.Gitlab({token:o,host:t}),f=(await u.MergeRequestNotes.all(i,s)).find((d)=>d.body.includes(r)),w=`${e}
---
${r}`;if(f)await u.MergeRequestNotes.edit(i,s,f.id,{body:w});else await u.MergeRequestNotes.create(i,s,w)}catch(o){throw c.error(`Failed to comment on PR: ${JSON.stringify(o)}`),o}};var ne=p(()=>{Me=require("@gitbeaker/rest");I();h()});var N="#### Powered by [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)",Ke,te,V,D,Pe=5;var _=p(()=>{Ke=[{model:"o3-mini",maxPromptLength:300000},{model:"o1",maxPromptLength:300000},{model:"gpt-4o-mini",maxPromptLength:300000},{model:"gpt-4o",maxPromptLength:300000},{model:"gpt-4-turbo",maxPromptLength:300000},{model:"gpt-4-turbo-preview",maxPromptLength:300000},{model:"gpt-4",maxPromptLength:21000},{model:"gpt-4-32k",maxPromptLength:90000},{model:"gpt-3.5-turbo",maxPromptLength:9000},{model:"gpt-3.5-turbo-16k",maxPromptLength:45000}],te={".js":"JavaScript",".ts":"TypeScript",".py":"Python",".sh":"Shell",".go":"Go",".rs":"Rust",".tsx":"TypeScript",".jsx":"JavaScript",".dart":"Dart",".php":"PHP",".cpp":"C++",".h":"C++",".c":"C",".cxx":"C++",".hpp":"C++",".hxx":"C++",".cs":"C#",".rb":"Ruby",".kt":"Kotlin",".kts":"Kotlin",".java":"Java",".vue":"Vue",".tf":"Terraform",".hcl":"Terraform",".swift":"Swift"},V=new Set(Object.keys(te)),D=new Set(["types"])});var Be=65536,j=(e)=>{let r=Ke.find((o)=>o.model===e)?.maxPromptLength;if(!r)return c.warn(`Model ${e} not found in predefined list. Using default max prompt length (${Be} chars).`),Be;return r};var ce=p(()=>{_();h()});class O{model;constructor(e){switch(e.provider){case"openai":this.model=new z.ChatOpenAI({apiKey:e.apiKey,...e.organization&&{organization:e.organization},temperature:e.temperature,modelName:e.modelName});break;case"azureai":this.model=new z.AzureChatOpenAI({temperature:e.temperature});break;case"bedrock":throw new Error("Bedrock provider not implemented");default:throw new Error("Provider not supported")}}async callModel(e){return(await this.model.invoke(e)).content[0]}async callStructuredModel(e,r){let i=await this.model.withStructuredOutput(r,{method:"jsonSchema",strict:!0,includeRaw:!0}).invoke(e);if(c.debug("LLm response",i),i.parsed)return i.parsed;return mo(i.raw.content[0])}}var z,mo=(e)=>{c.debug("Unparsed JSON",e);let r=e.replace(/\\/g,"\\\\").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t").replace(/```/g,"\\`\\`\\`").replace(/`/g,"\\`").replace(/"/g,"\\\"").replace(/\f/g,"\\f").replace(/\b/g,"\\b").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029");return c.debug("Escaped JSON",r),JSON.parse(r)};var se=p(()=>{z=require("@langchain/openai");h()});var v,ao,Ye;var Ve=p(()=>{v=require("zod"),ao=v.z.array(v.z.object({targetCodeBlock:v.z.string().describe("The exact code block that the feedback is about verbatim. Do not include any other text or comments in the code block. Do not include ``` or any other formatting."),suggestedChanges:v.z.string().describe("The full code block including any suggested changes if it makes sense to have them. Do not include any other text or comments in the code block. Do not include ``` or any other formatting. Do not include any + or - to indicate the changes.").optional(),reasoning:v.z.string().describe("The review of the code block and the reasoning for the suggested changes (if any). Justify the changes you are making to the code block as a code reviewer would. If there are no suggested changes, just write a review of the code block.")})),Ye=v.z.object({fileName:v.z.string().describe("The name of the file that the code changes are in"),riskScore:v.z.number().describe("A risk score from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe"),review:ao,confidence:v.z.number().describe("A confidence score from 1 to 5 about how confident you are in the risk score, where 1 is the lowest confidence and 5 is the highest confidence. Low confidence scores are for when you are not confident about the library being used or the code being safe. High confidence scores are for objective bad practise.")})});class De{items;constructor(e=[]){this.items=e}enqueue(e,r){let o={priority:r,item:e};this.items.push(o),this.items.sort((i,n)=>i.priority-n.priority)}dequeue(){return this.items.shift()?.item}size(){return this.items.length}peek(){return this.items[0]}getItems(){return this.items.map((e)=>e.item)}}var je;var ze=p(()=>{je=De});var vo=async(e)=>{try{return await e}catch(r){throw c.error("Error in processing prompt",r),r}},Eo=(e)=>{let r=new je,o=e.filter((i)=>i.riskScore>1&&i.confidence>3);for(let i of o)r.enqueue(i,i.riskScore*i.confidence);return r.getItems()},Ao=(e)=>{return e.reduce((r,o)=>{if(o.status==="fulfilled")return r.concat(o.value);return r},[])},Je=async(e,r)=>{let o=r.map((t)=>e.callStructuredModel(t,Ye)),i=await Promise.allSettled(o.map(vo)),n=Ao(i);return Eo(n)};var qe=p(()=>{h();Ve();ze()});var J=async(e,r,o,i,n)=>{c.info("Asking the experts...");let t=new O({modelName:r,temperature:0,apiKey:o,organization:i,provider:n}),s=await Je(t,e);if(s.length===0)return{markdownReport:"No issues found in PR \uD83C\uDF89",feedbacks:[]};return{markdownReport:Ne(s),feedbacks:s}};var fe=p(()=>{se();h();qe()});var Ze,We=(e)=>{let r=Ze.extname(e);return te[r]||"Unknown Language"};var Qe=p(()=>{Ze=require("path");_()});var ue=`You are an expert {ProgrammingLanguage} developer, your task is to review a set of pull requests.
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
Do not praise or complement anything. Only focus on the negative aspects of the code.
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
Ensure the feedback details are brief, concise, accurate, and in {ReviewLanguage}. If there are multiple similar issues, only comment on the most critical.
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
Respond in valid json making sure that all special characters are escaped properly:
- Code blocks should be escaped like this: \`\`\`typescript\\ncode here\\n\`\`\`
- Regular backticks should be escaped as \`
- Newlines should be escaped as \\n
- Double quotes should be escaped as \\"
Make sure your response can be parsed by JSON.parse().`;var Io=(e,r)=>{let o={},i=0,n=Number.POSITIVE_INFINITY,t=Number.NEGATIVE_INFINITY,s=new Map;for(let[u,g]of e.entries()){let f=g.trim(),w=s.get(f)||[];w.push(u),s.set(f,w)}for(let u of r){let g=u.substring(1).trim(),f=s.get(g);if(f?.length){let w=f.shift();if(w!==void 0)o[w]=u,i+=u.length+1,n=Math.min(n,w),t=Math.max(t,w)}}return{changedIndices:o,totalChangedLinesLength:i,minIndex:n,maxIndex:t}},_o=(e,r,o,i)=>{let n=Math.max(e-(i||0),0),t=Math.min(r+(i||0),o-1);return{start:n,end:t}},yo=(e,r,o,i)=>{let n=i,t=!0,s=!0,u=e,g=r;while(n>0&&(t||s)){if(t&&u>0){let f=o[u-1].length+1;if(f<=n)u--,n-=f;else t=!1}if(s&&g<o.length-1){let f=o[g+1].length+1;if(f<=n)g++,n-=f;else s=!1}if((u===0||!t)&&(g===o.length-1||!s))break;if(u===0)t=!1;if(g===o.length-1)s=!1}return{start:u,end:g}},Fo=(e,r,o,i)=>{let n=e>0?`...
`:"";for(let t=e;t<=r;t++)n+=`${o[t]||i[t]}
`;if(r<i.length-1)n+=`...
`;return n.trim()},q=(e,r,o)=>{return e.reduce((i,n)=>{let t=n.fileContent.split(`
`),s=n.changedLines.split(`
`),{changedIndices:u,totalChangedLinesLength:g,minIndex:f,maxIndex:w}=Io(t,s);if(g===0)return i;let d=r-g-n.fileName.length,{start:a,end:m}=_o(f,w,t.length,o);if(!o)({start:a,end:m}=yo(a,m,t,d));let L=Fo(a,m,u,t);return i.push({fileName:n.fileName,promptContent:L}),i},[])};var Xe=(e)=>e.fileName.length+e.promptContent.length;var G=(e,r)=>{let o=[],i=[],n=0;for(let t of e){let s=Xe(t);if(s>r)c.error(`Changes to file ${t.fileName} are larger than the max prompt length, consider using a model with a larger context window. Skipping file changes...`);else if(n+s>r)o.push(i),i=[t],n=s;else i.push(t),n+=s}if(i.length>0)o.push(i);return o};var Z=p(()=>{h()});var xe=(e,r)=>{let o=q(e,r);return G(o,r)};var Le=p(()=>{Z()});var Ce=(e,r)=>{let o=q(e,r,Pe);return G(o,r)};var er=p(()=>{_();Z()});var rr=(e,r)=>{let o=e.map((i)=>({fileName:i.fileName,promptContent:i.fileContent.split(`
`).map((n)=>`+${n}`).join(`
`)}));return G(o,r)};var or=p(()=>{Z()});var W=(e,r,o,i="English")=>{let n=r-ue.length,t;switch(o){case"full":t=rr(e,n);break;case"changed":t=xe(e,n);break;case"costOptimized":t=Ce(e,n);break;default:throw new Error(`Review type ${o} is not supported. Please use one of the following: full, changed, costOptimized.`)}let s=ue.replace("{ProgrammingLanguage}",We(e[0].fileName)).replace("{ReviewLanguage}",i);return t.map((g)=>{return s+JSON.stringify(g)})};var pe=p(()=>{Qe();Le();er();or()});var ir,ge=(e)=>{return e.filter((o)=>{let i=ir.extname(o.fileName);return V.has(i)&&![...D].some((n)=>o.fileName.includes(n))&&o.changedLines.trim()!==""})};var nr=p(()=>{ir=require("path");_()});var tr=p(()=>{nr()});var cr={};y(cr,{review:()=>$o});var $o=async(e,r,o)=>{c.debug("Review started."),c.debug(`Model used: ${e.model}`),c.debug(`Ci enabled: ${e.ci??"ci is undefined"}`),c.debug(`Comment per file enabled: ${String(e.commentPerFile)}`),c.debug(`Review type chosen: ${e.reviewType}`),c.debug(`Organization chosen: ${e.org??"organization is undefined"}`),c.debug(`Remote Pull Request: ${e.remote??"remote pull request is undefined"}`);let{ci:i,commentPerFile:n,model:t,reviewType:s,org:u,provider:g,reviewLanguage:f}=e,w=ge(r);if(w.length===0){c.info("No file to review, finishing review now.");return}c.debug(`Files to review after filtering: ${w.map((io)=>io.fileName).toString()}`);let d=j(t),a=W(w,d,s,f);c.debug(`Prompts used:
${a.toString()}`);let{markdownReport:m,feedbacks:L}=await J(a,t,o,u,g);if(c.debug(`Markdown report:
${m}`),i==="github"){if(!n)await B(m,N);if(n)await Se(L,N)}if(i==="gitlab")await Y(m,N);if(i==="azdev")await Re(m,N);return m};var sr=p(()=>{Te();ie();He();ne();ce();k();h();_();fe();pe();tr()});var fr,ko,Go=(e)=>{return!ko.includes(e)},Ro=(e)=>{let r=fr.extname(e);return V.has(r)&&![...D].some((o)=>e.includes(o))},ur=(e,r)=>{return Go(r)&&Ro(e)};var pr=p(()=>{fr=require("path");_();ko=["removed","unchanged"]});class we{client=new gr.Octokit({auth:Ie()});async fetchReviewFiles(e){let r=await this.client.paginate(this.client.rest.pulls.listFiles,{owner:e.owner,repo:e.repo,pull_number:e.prNumber});return await this.fetchPullRequestFiles(r)}async fetchPullRequestFiles(e){let r=[];for(let o of e){if(!ur(o.filename,o.status))continue;let i=await this.fetchPullRequestFile(o);r.push(i)}return r}async fetchPullRequestFile(e){let r=await this.fetchPullRequestFileContent(e.contents_url);return{fileName:e.filename,fileContent:r,changedLines:e.patch??""}}async fetchPullRequestFileContent(e){let r=await this.client.request(`GET ${e}`);if(To(r))return this.decodeBase64(r.data.content);throw new Error(`Unexpected response from Octokit. Response was ${JSON.stringify(r)}.`)}decodeBase64(e){return Buffer.from(e,"base64").toString("utf-8")}}var gr,To=(e)=>typeof e==="object"&&e!==null&&("data"in e)&&typeof e.data==="object"&&e.data!==null&&("content"in e.data)&&typeof e.data.content==="string";var wr=p(()=>{gr=require("octokit");I();pr()});var hr=(e)=>{let[r,o,i,n,t]=e.split(/(\/|#)/),s=Number.parseInt(t);return{owner:r,repo:i,prNumber:s}};var lr={};y(lr,{getRemotePullRequestFiles:()=>No});var No=async(e)=>{let r=hr(e),o=new we;try{return await o.fetchReviewFiles(r)}catch(i){throw new Error(`Failed to get remote Pull Request files: ${JSON.stringify(i)}`)}};var br=p(()=>{wr()});var dr,Oo=(e)=>`"${e.replace(/(["$`\\])/g,"\\$1")}"`,Uo=(e,r)=>{let o=Oo(r);if(e==="github"){let{githubSha:i,baseSha:n}=F();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}if(e==="gitlab"){let{gitlabSha:i,mergeRequestBaseSha:n}=$();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}if(e==="azdev"){let{azdevSha:i,baseSha:n}=U();return`git diff -U0 --diff-filter=AMRT ${n} ${i} ${o}`}return`git diff -U0 --diff-filter=AMRT --cached ${o}`},mr=async(e,r)=>{let o=Uo(e,r);return new Promise((i,n)=>{dr.exec(o,(t,s,u)=>{if(t)n(new Error(`Failed to execute command. Error: ${t.message}`));else if(u)n(new Error(`Command execution error: ${u}`));else{let g=s.split(`
`).filter((f)=>f.startsWith("+")||f.startsWith("-")).filter((f)=>!(f.startsWith("---")||f.startsWith("+++"))).join(`
`);i(g)}})})};var ar=p(()=>{dr=require("child_process");I();k()});var he,vr,So=(e)=>{if(e==="github"){let{githubSha:r,baseSha:o}=F();return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e==="gitlab"){let{gitlabSha:r,mergeRequestBaseSha:o}=$();return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e==="azdev"){let{azdevSha:r,baseSha:o}=U();return`git diff --name-only --diff-filter=AMRT ${o} ${r}`}if(e===void 0)return"git diff --name-only --diff-filter=AMRT --cached";throw new Error("Invalid CI platform")},Ho=()=>{return new Promise((e,r)=>{he.exec("git rev-parse --show-toplevel",(o,i)=>{if(o)r(new Error(`Failed to find git root. Error: ${o.message}`));else e(i.trim())})})},Er=async(e)=>{let r=await Ho();c.debug("gitRoot",r);let o=So(e);return c.debug("commandString",o),new Promise((i,n)=>{he.exec(o,{cwd:r},(t,s,u)=>{if(t)n(new Error(`Failed to execute command. Error: ${t.message}`));else if(u)n(new Error(`Command execution error: ${u}`));else{let g=s.split(`
`).filter((f)=>f.trim()!=="").map((f)=>vr.join(r,f.trim()));i(g)}})})};var Ar=p(()=>{he=require("child_process"),vr=require("path");I();k();h()});var yr={};y(yr,{getFilesWithChanges:()=>Mo});var Ir,_r,Mo=async(e)=>{try{let r=await Er(e);if(c.debug("fileNames",r),r.length===0)c.warn("No files with changes found, you might need to stage your changes."),_r.exit(0);let o=await Promise.all(r.map(async(i)=>{let n=await Ir.readFile(i,"utf8"),t=await mr(e,i);return c.debug("changedLines",t),{fileName:i,fileContent:n,changedLines:t}}));return c.debug("files",o),o}catch(r){throw new Error(`Failed to get files with changes: ${r.message}
${r.stack}`)}};var Fr=p(()=>{Ir=require("fs/promises"),_r=require("process");h();ar();Ar()});var $r={};y($r,{getReviewFiles:()=>Ko});var Ko=async(e,r)=>{if(r!==void 0){let{getRemotePullRequestFiles:i}=await Promise.resolve().then(() => (br(),lr));return await i(r)}let{getFilesWithChanges:o}=await Promise.resolve().then(() => (Fr(),yr));return await o(e)};var kr=`
Your role is to help testing a GPT application reviewing code changes. You receive a test case and you need to generate code in typescript corresponding to this test case, even if it follows bad practices or has security issues.
The test cases is formatted as a stringified JSON object with the following properties:
- name: the name of the test case
- description: the description of the test case
The input is the following:
{testCase}
Return the content of a valid typescript file that would pass the test case.
`,le=0.1,be="#### Tests Powered by [Code Review GPT](https://github.com/mattzcarey/code-review-gpt)";var Gr,Rr,Tr=async(e)=>{let r=new Gr.OpenAIEmbeddings;return await Rr.MemoryVectorStore.fromDocuments(e,r)};var Nr=p(()=>{Gr=require("@langchain/openai"),Rr=require("langchain/vectorstores/memory")});var Or,Ur,Sr,Po=async(e)=>{return await new Sr.TextLoader(e).load()},Hr=async(e)=>{let r=Or.readdirSync(e),o=await Promise.all(r.map(async(i)=>{return Po(Ur.default.join(e,i))}));return await Tr(o.flat())};var Mr=p(()=>{Or=require("fs"),Ur=l(require("path")),Sr=require("langchain/document_loaders/fs/text");Nr()});var Kr,Q,Bo=(e)=>typeof e==="object"&&e!==null&&("name"in e)&&typeof e.name==="string"&&("description"in e)&&typeof e.description==="string",Yo=async(e)=>{try{let r=await Q.readFile(e,"utf8"),o=JSON.parse(r);if(!Bo(o))throw new Error("File data is of unexpected format.");return o}catch(r){throw c.error(`Error loading test case: ${e}`),r}},Pr=async(e)=>{try{let r=(await Q.readdir(e)).filter((o)=>o.endsWith(".json"));return Promise.all(r.map(async(o)=>await Yo(Kr.default.join(e,o))))}catch(r){throw c.error(`Error loading test cases from: ${e}`),r}};var Br=p(()=>{Kr=l(require("path")),Q=require("fs/promises");h()});var Yr,Vo="sha256",Vr=(e)=>{return Yr.default.createHash(Vo).update(e).digest("hex")};var Dr=p(()=>{Yr=l(require("crypto"))});var X,jr,Do=async(e,r)=>{let o=kr.replace("{testCase}",JSON.stringify(e));return(await r.callModel(o)).replace("```typescript","").replace("```","")},jo=async(e,r,o)=>{if(e.snippet)return e;let i=Vr(e.description),n=jr.default.join(r,`${i}.ts`);try{let t=X.readFileSync(n,"utf8");return{...e,snippet:{fileName:n,fileContent:t,changedLines:t}}}catch(t){c.info(`Snippet not found in cache: ${e.name}. Generating it...`);let s=await Do(e,o);return X.writeFileSync(n,s,"utf8"),{...e,snippet:{fileName:n,fileContent:s,changedLines:s}}}},zr=async(e,r,o)=>{return Promise.all(e.map((i)=>jo(i,r,o)))};var Jr=p(()=>{X=require("fs"),jr=l(require("path"));h();Dr()});var A,qr,zo=(e)=>{if(e>1-le)return"PASS";if(e>1-2*le)return"WARN";return"FAIL"},Zr=(e,r)=>{switch(e){case"PASS":return A.default.green(`✅ [PASS] - ${r}`);case"WARN":return A.default.yellow(`⚠️ [WARN] - ${r}`);case"FAIL":return A.default.red(`❌ [FAIL] - ${r}`)}},Wr=(e,r,o,i)=>{let n=zo(i),t=n!=="PASS",s=Zr(n,`Test case: ${e.name} - Similarity score: ${i}
`)+(t?Jo(e,r,o):"");return{result:n,report:s}},Jo=(e,r,o)=>`
> Test case snippet: ${JSON.stringify(e.snippet)}
===============================================================================
> Review:
${r}
===============================================================================
> Similar review:
${o}
`,Qr=(e)=>{let r=Object.entries(e).reduce((i,[n,t])=>{return`${i+Zr(t,`Test case: ${n}`)}
`},A.default.blue(`
### Test results summary:
`)),o=Object.values(e).reduce((i,n)=>{return i[n]++,i},Object.fromEntries(Object.values(qr).map((i)=>[i,0])));return`${r}
**SUMMARY: ${A.default.green(`✅ PASS: ${o.PASS}`)} - ${A.default.yellow(`⚠️ WARN: ${o.WARN}`)} - ${A.default.red(`❌ FAIL: ${o.FAIL}`)}**
`};var Xr=p(()=>{A=l(require("picocolors"));((i)=>{i.PASS="PASS";i.WARN="WARN";i.FAIL="FAIL"})(qr||={})});var xr,qo=async(e,r,o,i,n,t,s)=>{if(!r.snippet)throw new Error(`Test case ${r.name} does not have a snippet.`);c.info(xr.default.blue(`Running test case ${r.name}...`));let u=W([r.snippet],i,t,s),{markdownReport:g}=await J(u,o,e,void 0,"openai"),f=await n.similaritySearchWithScore(g,1);if(f.length===0)throw new Error(`No similar reviews found for test case ${r.name}.`);let[w,d]=f[0],{result:a,report:m}=Wr(r,g,w.pageContent,d);return c.info(m),a},Lr=async(e,r,o,i,n,t,s)=>{if(r.length===0)return"No test cases found.";c.info(`Running ${r.length} test cases...
`);let u={};for(let f of r)try{let w=await qo(e,f,o,i,n,t,s);u[f.name]=w}catch(w){c.error(`Error running test case ${f.name}:`,w)}let g=Qr(u);return c.info(g),g};var Cr=p(()=>{xr=l(require("picocolors"));h();fe();pe();Xr()});var eo={};y(eo,{test:()=>Zo});var x,__dirname="/Users/matt/Documents/Github/code-review-gpt/src/test",Zo=async({ci:e,model:r,reviewType:o,reviewLanguage:i},n)=>{let t=j(r),s=await Pr(x.default.join(__dirname,"cases")),u=await zr(s,x.default.join(__dirname,"cases/.cache"),new O({modelName:r,temperature:0,apiKey:n,organization:void 0,provider:"openai"})),g=await Hr(x.default.join(__dirname,"cases/snapshots")),f=await Lr(n,u,r,t,g,o,i);if(e==="github")await B(f,be);if(e==="gitlab")await Y(f,be)};var ro=p(()=>{x=l(require("path"));ie();ne();se();ce();k();Mr();Br();Jr();Cr()});var oo=l(require("dotenv"));var fo=l(require("@inquirer/rawlist")),de=l(require("dotenv")),me=l(require("yargs")),ae=require("yargs/helpers");de.default.config();var ve=async()=>{return me.default(ae.hideBin(process.argv)).command("configure","Configure the tool").command("review","Review code changes").command("test","Run tests").demandCommand(1,"Please specify a command: configure, review, or test").option("ci",{description:"CI environment type",choices:["github","gitlab","azdev"],type:"string",coerce:(e)=>e||"github"}).option("setupTarget",{description:"Specifies for which platform ('github', 'gitlab' or 'azdev') the project should be configured for. Defaults to 'github'.",choices:["github","gitlab","azdev"],type:"string",default:"github"}).option("commentPerFile",{description:"Enables feedback to be made on a file-by-file basis. Only work when the script is running on GitHub.",type:"boolean",default:!1}).option("model",{description:"The model to use for generating the review.",type:"string",default:"gpt-4o-mini"}).option("reviewType",{description:"Type of review to perform. 'full' will review the entire file, 'changed' will review the changed lines only but provide the full file as context if possible. 'costOptimized' will review only the changed lines using the least tokens possible to keep api costs low. Defaults to 'changed'.",choices:["full","changed","costOptimized"],type:"string",default:"changed"}).option("reviewLanguage",{description:"Specifies the target natural language for translation",type:"string"}).option("remote",{description:"The identifier of a remote Pull Request to review",type:"string",coerce:(e)=>{return e||""}}).option("debug",{description:"Enables debug logging.",type:"boolean",default:!1}).option("org",{description:"Organization id to use for openAI",type:"string",default:void 0}).option("provider",{description:"Provider to use for AI",choices:["openai","azureai","bedrock"],type:"string",default:"openai"}).help().parse()};h();I();oo.default.config();var Wo=async()=>{let e=await ve(),r=Ae();switch(c.settings.minLevel=e.debug?2:e.ci?4:3,c.debug(`Args: ${JSON.stringify(e)}`),e._[0]){case"configure":{let{configure:o}=await Promise.resolve().then(() => (Ge(),ke));await o(e);break}case"review":{let{review:o}=await Promise.resolve().then(() => (sr(),cr)),{getReviewFiles:i}=await Promise.resolve().then(() => $r),n=await i(e.ci,e.remote);await o(e,n,r);break}case"test":{let{test:o}=await Promise.resolve().then(() => (ro(),eo));await o(e,r);break}default:c.error("Unknown command"),process.exit(1)}};Wo().catch((e)=>{let r=e instanceof Error?e.message:"An unknown error occurred",o=e instanceof Error?e.stack:"No stack trace available";if(c.error(`Error: ${r}`),o)c.debug(`Stack trace: ${o}`);process.exit(1)});