From 3b29aa828285ab8cc79ea45da7f5e58ca5442695 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 19:59:29 +0000 Subject: [PATCH 01/12] refactor --- src/ai_reviewer/diff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai_reviewer/diff.py b/src/ai_reviewer/diff.py index a6afbaf..2258d95 100644 --- a/src/ai_reviewer/diff.py +++ b/src/ai_reviewer/diff.py @@ -61,7 +61,7 @@ def run_git_diff(repo: str, base: str, head: str) -> str: def parse_diff(diff_text: str) -> list[FileDiff]: files: list[FileDiff] = [] current_path: str | None = None - header_lines: list[str] = [] + header_ lines: list[str] = [] hunks: list[Hunk] = [] current_hunk_header: str | None = None current_hunk_lines: list[str] = [] @@ -89,9 +89,9 @@ def parse_diff(diff_text: str) -> list[FileDiff]: if line.startswith("diff --git "): flush_file() header_lines = [line] - parts = line.split() - if len(parts) >= 4 and parts[3].startswith("b/"): - current_path = parts[3][len("b/") :] + p = line.split() + if len(parts) >= 4 and p[3].startswith("b/"): + current_path = p[3][len("b/") :] else: current_path = None continue -- 2.47.3 From e251e246d01c7d2365489668d0b3ef7e53b6f940 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:03:25 +0000 Subject: [PATCH 02/12] olama extend --- .github/workflows/ai-review.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 6fa0a70..53cc001 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -7,13 +7,8 @@ on: jobs: review: runs-on: ubuntu-latest - services: - ollama: - image: ollama/ollama:latest - ports: - - 11434:11434 - options: >- - --health-cmd="curl -sSf http://192.168.1.92:11434/ || exit 1" --health-interval=10s --health-timeout=5s --health-retries=12 + # Using an external Ollama server at 192.168.1.92:11434 + # Do NOT start a local Ollama service in the runner; the workflow will connect to the external host. steps: - name: Checkout -- 2.47.3 From 42aa92ab75483161a0a78270b501d313a30c00d1 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:09:50 +0000 Subject: [PATCH 03/12] test new python version --- .github/workflows/ai-review.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 53cc001..1ee560c 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -17,7 +17,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.11.x' + check-latest: true + + - name: Show Python + run: python --version - name: Create venv and install run: | -- 2.47.3 From 77af5617b92f6e47f3f88baec4d68c7c08f8e56a Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:11:17 +0000 Subject: [PATCH 04/12] python --- .github/workflows/ai-review.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 1ee560c..0bd1d75 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -23,6 +23,25 @@ jobs: - name: Show Python run: python --version + - name: Ensure Python 3.11 present (fallback) + run: | + set -e + echo "Checking python version..." + if command -v python >/dev/null 2>&1; then + python --version || true + fi + if python --version 2>&1 | grep -q "Python 3.11"; then + echo "Python 3.11 already installed" + else + echo "Attempting to install python3.11 via apt" + sudo apt-get update + sudo apt-get install -y python3.11 python3.11-venv python3.11-distutils python3-pip || true + if [ -x "/usr/bin/python3.11" ]; then + sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 || true + fi + python --version || /usr/bin/python3.11 --version || true + fi + - name: Create venv and install run: | python -m venv venv -- 2.47.3 From 4c29732a8e78c20c5dc42a0ad25e67cd627a3e97 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:13:55 +0000 Subject: [PATCH 05/12] New python --- .github/workflows/ai-review.yml | 70 ++++++++++++++------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 0bd1d75..af1e723 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -7,42 +7,24 @@ on: jobs: review: runs-on: ubuntu-latest - # Using an external Ollama server at 192.168.1.92:11434 - # Do NOT start a local Ollama service in the runner; the workflow will connect to the external host. + container: + image: python:3.11-bookworm steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11.x' - check-latest: true - - name: Show Python run: python --version - - name: Ensure Python 3.11 present (fallback) + - name: Install system deps (git + curl) run: | - set -e - echo "Checking python version..." - if command -v python >/dev/null 2>&1; then - python --version || true - fi - if python --version 2>&1 | grep -q "Python 3.11"; then - echo "Python 3.11 already installed" - else - echo "Attempting to install python3.11 via apt" - sudo apt-get update - sudo apt-get install -y python3.11 python3.11-venv python3.11-distutils python3-pip || true - if [ -x "/usr/bin/python3.11" ]; then - sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 || true - fi - python --version || /usr/bin/python3.11 --version || true - fi + apt-get update + apt-get install -y --no-install-recommends git curl ca-certificates + git --version + curl --version - - name: Create venv and install + - name: Create venv and install project run: | python -m venv venv . venv/bin/activate @@ -51,24 +33,25 @@ jobs: - name: Wait for Ollama run: | - for i in $(seq 1 30); do - if curl -sSf http://192.168.1.92:11434/ >/dev/null 2>&1; then - echo "ollama ready" && break + for i in $(seq 1 60); do + if curl -sSf http://192.168.1.92:11434/api/tags >/dev/null 2>&1; then + echo "ollama ready" && exit 0 fi sleep 1 done - - - name: (Optional) Pull model into Ollama - run: | - . venv/bin/activate - ollama pull qwen2.5-coder:7b || true + echo "ollama not reachable" >&2 + exit 1 - name: Run ai-reviewer env: OLLAMA_HOST: http://192.168.1.92:11434 run: | . venv/bin/activate - ai-reviewer review --repo . --base "${{ github.event.pull_request.base.ref }}" --head "${{ github.head_ref }}" --format json > review.json + ai-reviewer review \ + --repo . \ + --base "${{ github.event.pull_request.base.ref }}" \ + --head "${{ github.head_ref }}" \ + --format json > review.json - name: Post PR comment with findings uses: actions/github-script@v6 @@ -76,16 +59,21 @@ jobs: script: | const fs = require('fs'); let body = '{}'; - try { - body = fs.readFileSync('review.json', 'utf8'); - } catch (e) { + try { body = fs.readFileSync('review.json', 'utf8'); } catch (e) { body = JSON.stringify({ error: 'missing-review', message: String(e) }); } let parsed = {}; - try { parsed = JSON.parse(body); } catch (e) { parsed = { error: 'invalid-json', raw: body }; } + try { parsed = JSON.parse(body); } catch (e) { + parsed = { error: 'invalid-json', raw: body }; + } const findings = parsed.findings || []; - const summary = findings.length === 0 ? 'AI Reviewer: no findings.' : `AI Reviewer found ${findings.length} findings.`; - const commentBody = `${summary}\n\n
Full JSON\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n` + '```json\n' + JSON.stringify(parsed, null, 2) + '\n```\n
'; + const summary = findings.length === 0 + ? 'AI Reviewer: no findings.' + : `AI Reviewer found ${findings.length} findings.`; + const commentBody = + `${summary}\n\n
Full JSON\n\n` + + '```json\n' + JSON.stringify(parsed, null, 2) + '\n```\n' + + '
'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, -- 2.47.3 From 73787f6dfa7bef96b215f19a6374000ea9dcb727 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:15:25 +0000 Subject: [PATCH 06/12] new action test --- .github/workflows/ai-review.yml | 53 ++++++++++----------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index af1e723..21bf92b 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -8,21 +8,22 @@ jobs: review: runs-on: ubuntu-latest container: - image: python:3.11-bookworm + image: node:20-bookworm steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Show Python - run: python --version - - - name: Install system deps (git + curl) + - name: Install system deps + Python 3.11 run: | apt-get update - apt-get install -y --no-install-recommends git curl ca-certificates + apt-get install -y --no-install-recommends \ + git curl ca-certificates \ + python3.11 python3.11-venv python3-pip + ln -sf /usr/bin/python3.11 /usr/local/bin/python + python --version + node --version git --version - curl --version + + - name: Checkout + uses: actions/checkout@v4 - name: Create venv and install project run: | @@ -53,30 +54,8 @@ jobs: --head "${{ github.head_ref }}" \ --format json > review.json - - name: Post PR comment with findings - uses: actions/github-script@v6 - with: - script: | - const fs = require('fs'); - let body = '{}'; - try { body = fs.readFileSync('review.json', 'utf8'); } catch (e) { - body = JSON.stringify({ error: 'missing-review', message: String(e) }); - } - let parsed = {}; - try { parsed = JSON.parse(body); } catch (e) { - parsed = { error: 'invalid-json', raw: body }; - } - const findings = parsed.findings || []; - const summary = findings.length === 0 - ? 'AI Reviewer: no findings.' - : `AI Reviewer found ${findings.length} findings.`; - const commentBody = - `${summary}\n\n
Full JSON\n\n` + - '```json\n' + JSON.stringify(parsed, null, 2) + '\n```\n' + - '
'; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: commentBody, - }); + # Temporarily remove github-script; it also needs GitHub env + - name: Show result + run: | + ls -la + head -c 2000 review.json || true -- 2.47.3 From 1354e098312bb42e37bbc75eaaa672ed1701e3be Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:18:44 +0000 Subject: [PATCH 07/12] fix diff file --- .../__pycache__/diff.cpython-311.pyc | Bin 7968 -> 7968 bytes .../__pycache__/prompt.cpython-311.pyc | Bin 1391 -> 1391 bytes src/ai_reviewer/diff.py | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai_reviewer/__pycache__/diff.cpython-311.pyc b/src/ai_reviewer/__pycache__/diff.cpython-311.pyc index 646eced0a4e28c74c3f20f3350d3c59e40ed5112..18b212e380ebf3b68563fdac8758403594042d70 100644 GIT binary patch delta 20 acmZ2rx4@2jIWI340}#luHE!e str: def parse_diff(diff_text: str) -> list[FileDiff]: files: list[FileDiff] = [] current_path: str | None = None - header_ lines: list[str] = [] + header_lines: list[str] = [] hunks: list[Hunk] = [] current_hunk_header: str | None = None current_hunk_lines: list[str] = [] @@ -89,9 +89,9 @@ def parse_diff(diff_text: str) -> list[FileDiff]: if line.startswith("diff --git "): flush_file() header_lines = [line] - p = line.split() - if len(parts) >= 4 and p[3].startswith("b/"): - current_path = p[3][len("b/") :] + parts = line.split() + if len(parts) >= 4 and parts[3].startswith("b/"): + current_path = parts[3][len("b/") :] else: current_path = None continue -- 2.47.3 From fa494cfaaeb5b5e703d396abe5712b978d270e81 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:20:33 +0000 Subject: [PATCH 08/12] test git diff --- .../__pycache__/diff.cpython-311.pyc | Bin 7968 -> 8464 bytes src/ai_reviewer/diff.py | 23 +++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ai_reviewer/__pycache__/diff.cpython-311.pyc b/src/ai_reviewer/__pycache__/diff.cpython-311.pyc index 18b212e380ebf3b68563fdac8758403594042d70..d4c7db85bb2bd97a70d11592e4452af449e2766d 100644 GIT binary patch delta 997 zcmZXTO=uHA6o6+o*`Fl48)=iq*rrs|L(@_!Jy@i*VzKI>UBzm#{%vb#6SpRtGTF3M zqESyGIYie(z)L_-K@Vy!J*psh(v1WR3xXcKwz2%vmSsTVsZe9 zNXo&-Ajdog3H~N$z4e90Im9CY2`@cQN$!h3`4AYpqQDe)&I6zyU>5`nz;LgF4c7u# zha`VwgwOFS%uW>YuF$@))nzWzXKdL zu9?eM9WIUagl^pMCv>w9>G619d`VAgC?;68*qi4vCQ4^bhprmN4x2HtzUp|UHH(h0hS%FqxSFXe~%ezI4py_mNTe2MafCp{F4LqNQwkZwY^u2EIrG zRcWv$4cYnGD&{2?!*PpYS=wrYZl&YZA z1cfMMG{5-*4ZyI{UwjwDd+r9yuz9K3Y;JjhKU^?MbKB{%Qk9jOtlU0c=j9q7F3sC~ zl#IH+t8mK}B3m5rg^KHsZhZ|6eF+U!L&LSuupOFt)kR>B&s4p$HSetLp3QWSCwR>P)%*ato)QlSvX(m=?p`7(K-+~0WVqkz(W~Nlhhc4C9>di zR!?wDnxz&IpQV;1YcbdzJWtgN6e`6c#YL+a+|OPks#5t7)EKx}8QT4XftRh%dxp4H zi1Dy{kzFGZ4=P&seR#5UqK788s?8HE262)uT>H=zbs7{hMJp^#{gUHV)p*v-Vog;s z{lD-DYceuEO20FvU0_-WM<}`|=%x52Or+8%o75)p4YH-zI)f`OBKu&t7b>s;{^Jz1 Ie)Jyx4K0HMd;kCd delta 549 zcmbQ>w7`yUIWI340}#luHD>-5+{ib9n`s}zwYu@#r3q!tz3Vl6Hy$}9kL z^Gi!KS&M9dHWk@|2sL;(bfL?&nO332-Y`MN+{T*olE zmCw(70ow(3H6XgnCpE)tj_q}6y-U)17o`oZNE=*`cD-QYdXdlV3ZL5r4!0ZJf<3ku zSR{(XfZ8`72MM0C<#m#xWfJ{xE zB7cxb^ki>wY0(f6GZaKbf`}*(5jMG6+<-L=$h6(OK|Ga-F>14z)M6&ah{?NTwAsK) zGAF;5NnkAmDrwjpAlt=g4mOe#q#JBq0Fb50SdqJljD^6!WJ diff --git a/src/ai_reviewer/diff.py b/src/ai_reviewer/diff.py index a6afbaf..1eceeb3 100644 --- a/src/ai_reviewer/diff.py +++ b/src/ai_reviewer/diff.py @@ -53,9 +53,26 @@ def run_git_diff(repo: str, base: str, head: str) -> str: "--no-color", ] result = subprocess.run(cmd, check=False, capture_output=True, text=True) - if result.returncode not in (0, 1): - raise RuntimeError(result.stderr.strip() or "git diff failed") - return result.stdout + if result.returncode in (0, 1): + return result.stdout + + # If the diff failed because revisions are missing (common in CI when the + # PR head/base aren't fetched), try fetching from origin and retry once. + stderr = (result.stderr or "").strip() + try: + fetch_cmd = ["git", "-C", repo, "fetch", "origin", f"{base}", f"{head}"] + subprocess.run(fetch_cmd, check=False, capture_output=True, text=True) + except Exception: + # Ignore fetch errors; we'll raise the original error below. + pass + + # Retry diff once + result2 = subprocess.run(cmd, check=False, capture_output=True, text=True) + if result2.returncode in (0, 1): + return result2.stdout + + # If still failing, raise a helpful error including stderr from git + raise RuntimeError(result2.stderr.strip() or stderr or "git diff failed") def parse_diff(diff_text: str) -> list[FileDiff]: -- 2.47.3 From d35655ceebb04078f28ba187132618c6254ac972 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:25:27 +0000 Subject: [PATCH 09/12] fixed with codex --- .../__pycache__/cli.cpython-311.pyc | Bin 0 -> 6175 bytes .../__pycache__/diff.cpython-311.pyc | Bin 8464 -> 9350 bytes .../__pycache__/ollama.cpython-311.pyc | Bin 0 -> 1781 bytes .../__pycache__/render.cpython-311.pyc | Bin 0 -> 3638 bytes src/ai_reviewer/diff.py | 60 ++++++++++++------ .../test_diff.cpython-311-pytest-9.0.2.pyc | Bin 0 -> 7916 bytes tests/test_diff.py | 57 +++++++++++++++++ 7 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 src/ai_reviewer/__pycache__/cli.cpython-311.pyc create mode 100644 src/ai_reviewer/__pycache__/ollama.cpython-311.pyc create mode 100644 src/ai_reviewer/__pycache__/render.cpython-311.pyc create mode 100644 tests/__pycache__/test_diff.cpython-311-pytest-9.0.2.pyc create mode 100644 tests/test_diff.py diff --git a/src/ai_reviewer/__pycache__/cli.cpython-311.pyc b/src/ai_reviewer/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68d21ad3adee64de82461ea86beb1ed242a6fb4a GIT binary patch literal 6175 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 K(pF1dz<&Y7{fwLd literal 0 HcmV?d00001 diff --git a/src/ai_reviewer/__pycache__/diff.cpython-311.pyc b/src/ai_reviewer/__pycache__/diff.cpython-311.pyc index d4c7db85bb2bd97a70d11592e4452af449e2766d..02caa6b8b08011b986fa59b2f7aed0144fa5797d 100644 GIT binary patch delta 2035 zcmb7F&2JM&6rb7kde?URL7cVYBqnadmqTd6M|*04e2_v=LkdJyNQp^2>#$&KYjzD3 z)^ez#HU|!&i<%%>$yFst1qY;DDo)`)u#qdQRgt*#)T;6Ysp8Z(Yb7DARH>`o-+S}s z?c14o@Ar0b;Hwk9hhDE6!SzzSrT!Ye<7>sCJG=kr-9eh?9p~I+Cy_yV$Sx8A*+h1e z4v=25hr~(Oyl~D(xHBl;y?Owr-O_s)p(Uha^G~s@Sm1RSu3rZC07j2CECq+oug@W5 zAcHp?55V$h!}>XgP(dg-4dE7N2xpMtEQl^dFyRbDc;Yay!Oe;VNf$QbT-tcsC`v>i zPU3tlK5#%-cp4W*nLroPrS&mFI}qhDnm`%HHFTYyK-cj9U7GIMjAl4D@^&!)MP-`2ZE$Kj$>w#7Nrh6&lTxXsCqT_o z%VlpVk-S1Q7sQL1Q@)*3zkoJ}UWWFr3{^t=tD*gKr?w1!leC#^ppT5Bn{44bX~BNGyW=>HYt zwG(;5i&x9x{!%+Yxv#(C9;mtp%8mhT8?5i?JN+H#S%v$%i_ME4n}Wd0Cnk)af~CRl4Vu({YCDiZL!kG}s$#bab(pb9s7Lu}-{H_lmZ zO`oQ^#*#?ihY!U$Iu62eZzfs;9-vr^wt%3=*WqJOv>|{wRBM(O<++iX=&mCy>|LBZ zxODiN!=-p7*i#Mm*tVL~{6dO6mmG@n*nbdR|Gh(CJjM5VW{d|%)$J$yxu)t*rwqM{4JcQ0C3l4(=IaLq{M|fbT!*JY3 zL@)$61s+H!0#BXhnkVdX5{bBo>#^G`H%0e(t|IC+#7))`oZ^xkM(hE`isdZW0d8O9AL3rn&|!89I9-EvYz%cpgXb(C1F7_+ZN zwob4wFkm6IX8@3`Z9&LYOIIBQgo8FLNVV48b>w%!5Mbf>tAJb!wAgp37HlsEy5TNK z4Ot6BYk`isUkdsb246R$Q0(qhX>cXKcD6irwk#LNEAn_%9tRHj<*Gkc8ZP_0;jV?F zcMGMNa(GWUu;qT`mv0xKNW2GY(XQ{#zliodkM>of2ddEni|)Vboorb#)D&23D|_0@ zj&{3LESKGTbmhAK#+Oz#eR3F@)e!Wf)mQ#50jF0Tas=bc=JwEWteLYRb7T^B4{7m& zptDRDq4;xYRZpuK=w{BeodfzAOhG=_1ZC8;SwW}R3r&S!zYg1DoOwnC3t Hi5>YHhk_0y delta 1167 zcma)5O=ufO6rS1D{&=O`m0kTOvK`q?EgM59lu*((shv6rSR%!ZDbA1VdRERR@=DCE zoMPlyEukk1k>(UEDVsk)0KNtYBCZAyUxT*+3e>pU;9DLENL-ONc!y^6#&=j|2CzgUd@nKw=A z)Bjlt0xLWMoDi`>>5B)#0QeswrJD4LNa^1q9la+Q0P(7MJ$pZ`v|lAb+T@Z;sKk)WNT^>dB$5X6mY>Ue#o!wG^K^P;K7={uwMrH6@KwEOm6f`2?;i(Gjw@3E{%0Oi^!r#x3Xbuq z^HM9BcJsG^Nde9`!-*t>m&kbXEWAwYB>C_Ix}Ek{r4E10xJ0{GDVa{mv<%x9OWp3! zn6rc(`-bUUr43{2xSwr#i$RX<3*iflFOl=9L(v6BFES`FSY&XC)KkZV>y+l3Kc);0 zE;bLP?{e@8c|0^Bc2g|U8Cn$9Xv$9W!tgin)I6nGh)c|g`>$E099s-D23_H0wyye0 zp-`;Sdu9|0nEj*pEU_}DL{<{7kw=-dvQ827!p2@)EAk4!Kcm<~*IWv0lAIeB8 Ar~m)} diff --git a/src/ai_reviewer/__pycache__/ollama.cpython-311.pyc b/src/ai_reviewer/__pycache__/ollama.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..416f42d70b9071a54eb3061fe8979e71dbdf7144 GIT binary patch literal 1781 zcmZ`3U27XhaQD7+mL-40k)5v=CqgZ%9UrB22_=-8LR$LZKIEZ9DIAA(ZJ#;aUG`RS z981Ixc~FZ>gDI$x7WCvgB!43RKq`WYgMy(@$eV&wFs2Wky|ZMic4u#AXJ=;TV|V6$ zO{bFx;QN0z?WYn#e~L{Hl$gN#p8`=w1QG0@5-wmYXvvXEazT!0*-;9Lgg!?^d5nm< z1D2k{jG>?s`8LXHuf#Tw!(q#HJ#KN^a|77Q3-0=2@klpXj!xqJw_#gH6csQ*1&Jhy z`dBH*WSHo%Dr5+(roz-$j$@Uq&m5b&JXBV^fQNFxS*S6}%ghaxMbD+hemG!Ljwjy# zIC4B9wM z#Vb*R(EEnZyj5D{fw63JY8gexzHXJd$LM{Vt{DL<8kTLwJIV|%%5KhI4|UVDU7MTc z!_3K)=X&fR;qD@!P1MO;tl!Vz7^QSy0FTt*Kx zAYhO8Z$YAUR6(mf33>{z|BFl`Pg+5p}RSl{P#vm6XcQ}jQM}e{?J&^C9HK@ir2*apojiZV^wkxO|n9uvH zNKfr=PY{E8)dWu;3oD6+c+FElhDdTSzQLTKNQM_5rVY!tjb-XmW^sBe9Oks-3%blG z6E~Ju*aR%>JfKj6Vxm?lOn83OBd=}>4IC!8U7}u@1F5VAo*U|n20kHh8bEz|Vr89ZC zb0OEb*PP0ChtzDcivZOUf2YvM#MU>BYt3}7mCm)(xh}#(AMH+drrxS=Y;QC^*!}v+ zMr&%mJvCqZqAP^*(#cm3*b`qh@6ZR^>lp6!fI{ZRd` z`t75wM>XY-(Q{8etl!wa@za;PQfp$SJu%Z7oo$cKHuc$H98!IDVIeKQOe+iHiI?Xw z;6-?7QAg3;Gtsb2{C@LVeD~s3qZ&9N7reYGn%}ib)HK7SX+i^+9YLo|^Iq9PoCKGKP zYJAY~=b!K--4!NW#%VsP`uFA-25Lyx$*bZtthLyCkQyVf5{)@#m6qP$!( zyGy4gC}0ivAPFkO0s(BG3BZHlI&cm?6h)3X^am^h0$D6zz(7yF(RB}n&_mxW$rULk zg*=>n^JeDF*ZbyQW3dQ=@~8h=`TqnE`X}pj3-npRZ`J`mL=;i1q5@vPnBkntEpX8C zDqj#51aC}G1BKv15F-vLK^pij_yi;L48C^Z&}WE-9w17da4-&EyF7vBS+|20Li7NQ zJrEWM9iee(!_W>x8=>4alsfo|QKqmf%9>U*WiwyYj4brYB~#Jm+o}S1n98P{Q)R=r z4!z)#UaTlu&OZVWzTLrZE(7uqDQE#xw7}hfM=Ct!&cUdM@jfQ_Sir}E9wyQ-Tu-18 zxL(u^UCe7VuPwX0Vd}21lvfp3SSf3F+>oK=OC`m0Lj}b!>X=Il3rXpu@YJNs8 zn?+r@n^(Tf7iNACV%8<=()~-$E;4M*Tl4ql9l=M2 ztjpHr`FD7nheQb%Qb(i$*~$kHS1tKt*$dWi9_j87vCa zt>6(G%o38Ucye^(RH5aPB6XRnzYp_F!iEgMLNCOjjgPiIvgVwl0z&YLn|9uYc@B4>=^dO*TpG;6`jKwjOiH&~|JyR-+9v)+A%L zR~_+yb)_xWFdWqdR*!|lTjUHE`sszAzFs>7Q<_}ig#VbpSdmYTKX6eFc&IGG@mmiDaE;5`a}IU zFl;cPcX+!KImZ++S1gpySAN|#W@*(ZYUy)oF(<3Wc|f`Z@3F=207%)? zulQY$SFhLs*Wtt str: - cmd = [ - "git", - "-C", - repo, - "diff", - f"{base}...{head}", - "--unified=3", - "--no-color", - ] - result = subprocess.run(cmd, check=False, capture_output=True, text=True) - if result.returncode in (0, 1): - return result.stdout + base_candidates = _build_ref_candidates(base) + head_candidates = _build_ref_candidates(head) + + stdout, stderr = _diff_with_candidates(repo, base_candidates, head_candidates) + if stdout is not None: + return stdout # If the diff failed because revisions are missing (common in CI when the # PR head/base aren't fetched), try fetching from origin and retry once. - stderr = (result.stderr or "").strip() + first_error = stderr try: - fetch_cmd = ["git", "-C", repo, "fetch", "origin", f"{base}", f"{head}"] + fetch_cmd = ["git", "-C", repo, "fetch", "origin", base, head] subprocess.run(fetch_cmd, check=False, capture_output=True, text=True) except Exception: # Ignore fetch errors; we'll raise the original error below. pass - # Retry diff once - result2 = subprocess.run(cmd, check=False, capture_output=True, text=True) - if result2.returncode in (0, 1): - return result2.stdout + stdout, stderr = _diff_with_candidates(repo, base_candidates, head_candidates) + if stdout is not None: + return stdout # If still failing, raise a helpful error including stderr from git - raise RuntimeError(result2.stderr.strip() or stderr or "git diff failed") + raise RuntimeError(stderr or first_error or "git diff failed") + + +def _build_ref_candidates(ref: str) -> list[str]: + candidates = [ref] + if ref and not ref.startswith("origin/"): + candidates.append(f"origin/{ref}") + return candidates + + +def _diff_with_candidates(repo: str, base_candidates: list[str], head_candidates: list[str]) -> tuple[str | None, str]: + last_error = "" + for base_ref in base_candidates: + for head_ref in head_candidates: + cmd = [ + "git", + "-C", + repo, + "diff", + f"{base_ref}...{head_ref}", + "--unified=3", + "--no-color", + ] + result = subprocess.run(cmd, check=False, capture_output=True, text=True) + if result.returncode in (0, 1): + return result.stdout, "" + + if result.stderr: + last_error = result.stderr.strip() + return None, last_error def parse_diff(diff_text: str) -> list[FileDiff]: diff --git a/tests/__pycache__/test_diff.cpython-311-pytest-9.0.2.pyc b/tests/__pycache__/test_diff.cpython-311-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f12f2870f5091ee2b3061b111562c2892c29542b GIT binary patch literal 7916 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 EUpdwgW&i*H literal 0 HcmV?d00001 diff --git a/tests/test_diff.py b/tests/test_diff.py new file mode 100644 index 0000000..2c6fbf6 --- /dev/null +++ b/tests/test_diff.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +import subprocess + +from ai_reviewer.diff import run_git_diff + + +def _completed(cmd: list[str], returncode: int, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess[Any]: + return subprocess.CompletedProcess(cmd, returncode, stdout, stderr) + + +def test_run_git_diff_prefers_direct_refs(): + calls: list[list[str]] = [] + + def fake_run(cmd, check=False, capture_output=False, text=False): # type: ignore[override] + calls.append(cmd) + if cmd[3] == "diff": + if cmd[4] == "base...head": + return _completed(cmd, 0, stdout="ok") + return _completed(cmd, 128, stderr="fatal") + raise AssertionError("fetch should not run when refs exist locally") + + with patch("ai_reviewer.diff.subprocess.run", side_effect=fake_run): + output = run_git_diff(".", "base", "head") + + assert output == "ok" + # Only one diff attempt should be needed when local refs exist. + assert len(calls) == 1 + + +def test_run_git_diff_falls_back_to_origin_refs_after_fetch(): + fetched = False + diff_attempts: list[str] = [] + + def fake_run(cmd, check=False, capture_output=False, text=False): # type: ignore[override] + nonlocal fetched + if cmd[3] == "diff": + diff_attempts.append(cmd[4]) + spec = cmd[4] + if spec == "origin/base...origin/head" and fetched: + return _completed(cmd, 0, stdout="remote-ok") + return _completed(cmd, 128, stderr="missing ref") + if cmd[3] == "fetch": + fetched = True + return _completed(cmd, 0) + raise AssertionError("unexpected git invocation") + + with patch("ai_reviewer.diff.subprocess.run", side_effect=fake_run): + output = run_git_diff(".", "base", "head") + + assert output == "remote-ok" + assert "origin/base...origin/head" in diff_attempts + # Ensure the fallback only succeeds after fetch. + assert diff_attempts.index("origin/base...origin/head") > diff_attempts.index("base...head") -- 2.47.3 From 27af18a734337d049af74d7ff9731f72ce4a178f Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:29:38 +0000 Subject: [PATCH 10/12] fix --- .../__pycache__/diff.cpython-311.pyc | Bin 9350 -> 11080 bytes src/ai_reviewer/diff.py | 70 +++++++++++++----- .../test_diff.cpython-311-pytest-9.0.2.pyc | Bin 7916 -> 11107 bytes tests/test_diff.py | 33 ++++++++- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/src/ai_reviewer/__pycache__/diff.cpython-311.pyc b/src/ai_reviewer/__pycache__/diff.cpython-311.pyc index 02caa6b8b08011b986fa59b2f7aed0144fa5797d..9ae434fba37df60fcc05315c00ff84388d799272 100644 GIT binary patch delta 2928 zcmZ`*Yit|G5#Hn7kw+dMlKGG*nI`qJMOmUAwu2%PACnW^;tfIH(2n zSNz;CjZmJ=Tk|%Byf6yap%m; z)-H~%U1WaC0wDz{HpTHKEL^c5bPa~tz6TOKo3H!0wlv_wFSCkunN~RU3+EhVQ~O<$ zLv8U?B9oa*Bp2gCI+co_P0MF;D{_2E#Pec&E+L6k=gF0~ROM5Ea$F=PReMU5lV{?|C864~-7cO;$s&#$`s%|y3^&OMM~y@x>OWi|{#B6s zU`PQ<^|-^$UC-a#|A(-4GEwfDt8~qknfBGO;#g@)9dR!dhF+v}&RNW@7rvq`L)L#I zfHD&!7Md?svm(x|*@C*qUvzyLI!sKnTKKuL`#CV*Ma*}Bc?+0NZL|H474c1_A81Xd zt_A>Z90brtPL&GHFSkkyn;8;*2EY-Uz_#>};|g0AS0(fZx==S}25m0-=+q z=TRO4XB5q@=2^u8^Y`;s8MIpsLISQ=uzXHmVIU-6m<__vwotbd4l!>a?1rU!%m~yx zO3_IRg~)(twLOeQJpSmf4MzRS+hOMvE>8dz z@WgBNr&4H5S-T)0g;tmp4f$f7s-o%7sH$B$zqBOEIGy~=g2{<+1HhZEY%XF%V1z_q z)K>M!@k%y6pO(!iP3D%C5?P_@z@of@v&ozw;uFOCBtivLW#Nscm#b_hcNRkH04Sn% z{4JudUq;oDNz?<@o6S8E@NqMwIr8W@7E@L3jF=D%$%;j3B_mg9*jBQ`h7hl2!#=Y8 zF@S=BuYe@|4PX^PQ0?%oPUwUCG^R=Sb<~i9JxZBa56~4I0n&M@kBPpG*1t7y(x4#zcF{UU2E#q8n*TOqQ}3MuOY^Dl+pt` zFE8BaFqJ=uQ*&6z#g?dB%eR!|cFnLF= zjawlBAU%8?NTEJLK=PK4(9h_{^B_y|gd%woBt_tfJTbJl{QhxPus*4MO2J?Ro4^U& z=l1QH4|(R8QeS9%rofo5cR!pPPY{>{h_Yr*JoaX2;BAQ%CL^%TxK0uu0$;$8E&>=~ z?QAS{t~2W?&C#`bM7K565Y0w)j=$u1-%&cFdHbMmJga#NL@0By3K!Fie*YVr@pZ7; zQ|7uWT(`z`>x}J(6K_qFTgWL2pT_G<|ixsq{d9* z0Gy=C8u|Lb|9@%qK>&zW-Pho2tMeWB*kBkp$-uoD^lFhQHQ{aU%+m(PE*OQB0j6aX zHf70M6{|u6%Ta4?g;vAi?uAFM0;m_voO}mFN){!B4{L1e@tSFL`Vn}5Ppfc5zvXjHGn#`-D~Uo*#C)tbyVL4>}F^C z4JdOhMXuDXdz#jpHFtZF)_HfSvv{z0Pt5sT4f*P;H_qKY zpf#Q-H=d|8o+w&%zOlmZ+^}l=w|8s&ZrvNad}yOx^LA>^Z#r149jYP9-meG4KMmat z_TCBhmV>cMFjk!STIW4LS$wO;w5r$JhM+9pZwnWU8bH3KjH-+c$whw70z?^;eF(Xm zB$g$s6n>jbR+E4(66HGt+yo5gcZuF%zG1`JNGn4!SjUhE^$g-4)txR8C+j0l#@$@5 z>wir2jp|JMRG54b;c)`T2oUze9}pmEiC>Palu$}r2sZNh$vxnMQLr(!CHU9@DQsIUG delta 1423 zcmZWpU2Gdg5Z*oCpMTE2{G6S{Hcm)O?3B{Dh` zXJ=<;cJABP-a8q*ttdV~>Atj_|1oks*apMbdwxH(4>hmw7kp?x(kS<%XHYxk0n~$H zlq)ESQmA`HxDZ6#d64R@j>4i3K1aUtPao$1C<|rL5|%kjI1enbEO`Nfh_e9lAl`x& zHzSp0Q((t7sNmU=ESXFPE_VIThw0y02h`t!$C5~!G7Uv?O*tJ3jF#ri{H*>m#`8Gk zC6AQ!kUy8lhMCcs%JJrOGkYnMokfP1`sv&u+)8Io0GH-6GkG(E^0^!yqpE}SGrkAp z%V45a>%DXAv&6kaTiWQhHo82rbfR*C{1q&m2_JZJp(ZJ|)Lsi|73HA_{E@XQc4S~9 zO2O_M*z%>feQBFd8%@;z7hMA}uo?3Wq&%BRIFKU0g+7PlWL@14Um;(sqy7q5r$dDgXm-mU3l4#aY?-$>$|uVL;*f`CoVWO!011|G6MW=B zB9dtCMHA9(|4?C9wnQW&FYChG34%auBeO z7F;UqZpH2Yoso0lF!?DFYWA#VRuKZ0C&yk-V8j9B)qT$|a+n>0RSGE)pJMW93XTu! zW(m(_=aG)jGVM906$}&S7g93rXR6(NIlki5Zt3OeDMQa-J?F@m^eN;@9{La*5$i^& zXc{uzyYn6v-3NhBGItD^OJkG55~ww`ES;*gcGrO_9Ce{0*V=mOAmpV}3YFmps#a55 z-4U*}L~XT~jt#lNYihiv#_A!tC0H4J*bKs*Hzqd*@09M1*=NUWZG1}`-_~gTKW&Mm z?MTvA_lyT2ZM9qnP&rzQcdt+U5bygg-nSJ$vK>FN=6h66upl}?Kwayw{ZX5bR*$!) z1z4=U+ujc0WwPEe3=Q&g2T5L}mq~G0p$yM3S#o^FLNRX^^K-i4h($N0_;s=o)n1rn z1_6wi@d5=$##3)+n5DzJC@f52)4+LVP+%~}V4lHc@<+5!c!!Fp+7r_`IA4ALsgF50 zOAdD(5M2|COn1E{tWcBN)o;2cAo;cDAk9D|nRwH!C2M)N0kA8%F~31|DZUXBYk|g1 zbf$&)Dq~{f{A+B{Vi08DZZ5O+5cwrJ($Bga4>NF!&Emu?5*%j`X2AXpkLCW^c~mOu zFX9TFvPLmJtX@q$4PT1DL0AV*;4vPC>tHVgTVSjX_QJE|e(&KRh{5JM1NX?k0aBP= A3jhEB diff --git a/src/ai_reviewer/diff.py b/src/ai_reviewer/diff.py index f677305..5d0bf7c 100644 --- a/src/ai_reviewer/diff.py +++ b/src/ai_reviewer/diff.py @@ -46,7 +46,7 @@ def run_git_diff(repo: str, base: str, head: str) -> str: base_candidates = _build_ref_candidates(base) head_candidates = _build_ref_candidates(head) - stdout, stderr = _diff_with_candidates(repo, base_candidates, head_candidates) + stdout, stderr, fallback_pairs = _diff_with_candidates(repo, base_candidates, head_candidates) if stdout is not None: return stdout @@ -60,12 +60,20 @@ def run_git_diff(repo: str, base: str, head: str) -> str: # Ignore fetch errors; we'll raise the original error below. pass - stdout, stderr = _diff_with_candidates(repo, base_candidates, head_candidates) + stdout, stderr, fallback_pairs_after_fetch = _diff_with_candidates( + repo, base_candidates, head_candidates + ) + if stdout is not None: + return stdout + + stdout, fallback_error = _fallback_diff_without_merge_base( + repo, fallback_pairs + fallback_pairs_after_fetch + ) if stdout is not None: return stdout # If still failing, raise a helpful error including stderr from git - raise RuntimeError(stderr or first_error or "git diff failed") + raise RuntimeError(fallback_error or stderr or first_error or "git diff failed") def _build_ref_candidates(ref: str) -> list[str]: @@ -75,26 +83,24 @@ def _build_ref_candidates(ref: str) -> list[str]: return candidates -def _diff_with_candidates(repo: str, base_candidates: list[str], head_candidates: list[str]) -> tuple[str | None, str]: +def _diff_with_candidates( + repo: str, base_candidates: list[str], head_candidates: list[str] +) -> tuple[str | None, str, list[tuple[str, str]]]: last_error = "" + no_merge_base_pairs: list[tuple[str, str]] = [] for base_ref in base_candidates: for head_ref in head_candidates: - cmd = [ - "git", - "-C", - repo, - "diff", - f"{base_ref}...{head_ref}", - "--unified=3", - "--no-color", - ] - result = subprocess.run(cmd, check=False, capture_output=True, text=True) + result = _run_git_diff_command(repo, base_ref, head_ref, symmetric=True) if result.returncode in (0, 1): - return result.stdout, "" + return result.stdout, "", [] if result.stderr: - last_error = result.stderr.strip() - return None, last_error + err = result.stderr.strip() + last_error = err + if "no merge base" in err.lower(): + no_merge_base_pairs.append((base_ref, head_ref)) + + return None, last_error, no_merge_base_pairs def parse_diff(diff_text: str) -> list[FileDiff]: @@ -193,3 +199,33 @@ def chunk_files(files: Iterable[FileDiff], max_lines: int = 350) -> list[DiffChu ) return chunks + + +def _run_git_diff_command( + repo: str, base_ref: str, head_ref: str, *, symmetric: bool +) -> subprocess.CompletedProcess[str]: + cmd = ["git", "-C", repo, "diff"] + if symmetric: + cmd.append(f"{base_ref}...{head_ref}") + else: + cmd.extend([base_ref, head_ref]) + cmd.extend(["--unified=3", "--no-color"]) + return subprocess.run(cmd, check=False, capture_output=True, text=True) + + +def _fallback_diff_without_merge_base( + repo: str, pairs: list[tuple[str, str]] +) -> tuple[str | None, str]: + last_error = "" + seen: set[tuple[str, str]] = set() + for base_ref, head_ref in pairs: + key = (base_ref, head_ref) + if key in seen: + continue + seen.add(key) + result = _run_git_diff_command(repo, base_ref, head_ref, symmetric=False) + if result.returncode in (0, 1): + return result.stdout, "" + if result.stderr: + last_error = result.stderr.strip() + return None, last_error diff --git a/tests/__pycache__/test_diff.cpython-311-pytest-9.0.2.pyc b/tests/__pycache__/test_diff.cpython-311-pytest-9.0.2.pyc index f12f2870f5091ee2b3061b111562c2892c29542b..70cd5e8d3ea83e955cdd32238fd33ee642b7d743 100644 GIT binary patch delta 2916 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 Oe*;O9D)(>h&Hn<+w8D}A diff --git a/tests/test_diff.py b/tests/test_diff.py index 2c6fbf6..604e808 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -38,8 +38,11 @@ def test_run_git_diff_falls_back_to_origin_refs_after_fetch(): def fake_run(cmd, check=False, capture_output=False, text=False): # type: ignore[override] nonlocal fetched if cmd[3] == "diff": - diff_attempts.append(cmd[4]) - spec = cmd[4] + if len(cmd) == 7: + spec = cmd[4] + else: + spec = " ".join(cmd[4:6]) + diff_attempts.append(spec) if spec == "origin/base...origin/head" and fetched: return _completed(cmd, 0, stdout="remote-ok") return _completed(cmd, 128, stderr="missing ref") @@ -55,3 +58,29 @@ def test_run_git_diff_falls_back_to_origin_refs_after_fetch(): assert "origin/base...origin/head" in diff_attempts # Ensure the fallback only succeeds after fetch. assert diff_attempts.index("origin/base...origin/head") > diff_attempts.index("base...head") + + +def test_run_git_diff_fallbacks_to_two_dot_when_merge_base_missing(): + fetched = False + diff_attempts: list[str] = [] + + def fake_run(cmd, check=False, capture_output=False, text=False): # type: ignore[override] + nonlocal fetched + if cmd[3] == "diff": + if len(cmd) == 7: + spec = cmd[4] + diff_attempts.append(f"sym:{spec}") + return _completed(cmd, 128, stderr="fatal: origin/main...origin/head: no merge base") + spec = " ".join(cmd[4:6]) + diff_attempts.append(f"two:{spec}") + return _completed(cmd, 0, stdout="linear-diff") + if cmd[3] == "fetch": + fetched = True + return _completed(cmd, 0) + raise AssertionError("unexpected git invocation") + + with patch("ai_reviewer.diff.subprocess.run", side_effect=fake_run): + output = run_git_diff(".", "base", "head") + + assert output == "linear-diff" + assert any(attempt.startswith("two:") for attempt in diff_attempts) -- 2.47.3 From 2aa3e7cddf74e56203f33e79e6910ca598378002 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:38:38 +0000 Subject: [PATCH 11/12] change timeout --- src/ai_reviewer/ollama.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai_reviewer/ollama.py b/src/ai_reviewer/ollama.py index 6fbc722..6fed18f 100644 --- a/src/ai_reviewer/ollama.py +++ b/src/ai_reviewer/ollama.py @@ -17,7 +17,7 @@ class OllamaClient: "stream": False, "options": {"temperature": 0}, } - with httpx.Client(timeout=60) as client: + with httpx.Client(timeout=600) as client: response = client.post(url, json=payload) response.raise_for_status() data = response.json() -- 2.47.3 From f436c09c08323d98269e84799580245228d3f0c1 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Mon, 2 Feb 2026 20:53:13 +0000 Subject: [PATCH 12/12] set even longer timeout --- src/ai_reviewer/ollama.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai_reviewer/ollama.py b/src/ai_reviewer/ollama.py index 6fed18f..7b4bcc9 100644 --- a/src/ai_reviewer/ollama.py +++ b/src/ai_reviewer/ollama.py @@ -17,7 +17,7 @@ class OllamaClient: "stream": False, "options": {"temperature": 0}, } - with httpx.Client(timeout=600) as client: + with httpx.Client(timeout=3600) as client: response = client.post(url, json=payload) response.raise_for_status() data = response.json() -- 2.47.3