ai-cr 0.1.0__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_code_review/.ai-code-review.toml +305 -0
- ai_code_review/__init__.py +0 -0
- ai_code_review/bootstrap.py +37 -0
- ai_code_review/cli.py +112 -0
- ai_code_review/constants.py +7 -0
- ai_code_review/core.py +114 -0
- ai_code_review/project_config.py +99 -0
- ai_code_review/report_struct.py +108 -0
- ai_code_review/utils.py +75 -0
- ai_cr-0.4.3.dist-info/METADATA +161 -0
- ai_cr-0.4.3.dist-info/RECORD +14 -0
- ai_cr-0.4.3.dist-info/entry_points.txt +3 -0
- .git/COMMIT_EDITMSG +0 -1
- .git/FETCH_HEAD +0 -1
- .git/HEAD +0 -1
- .git/ORIG_HEAD +0 -1
- .git/config +0 -57
- .git/description +0 -1
- .git/hooks/applypatch-msg.sample +0 -15
- .git/hooks/commit-msg.sample +0 -24
- .git/hooks/fsmonitor-watchman.sample +0 -174
- .git/hooks/post-update.sample +0 -8
- .git/hooks/pre-applypatch.sample +0 -14
- .git/hooks/pre-commit.sample +0 -49
- .git/hooks/pre-merge-commit.sample +0 -13
- .git/hooks/pre-push.sample +0 -53
- .git/hooks/pre-rebase.sample +0 -169
- .git/hooks/pre-receive.sample +0 -24
- .git/hooks/prepare-commit-msg.sample +0 -42
- .git/hooks/push-to-checkout.sample +0 -78
- .git/hooks/sendemail-validate.sample +0 -77
- .git/hooks/update.sample +0 -128
- .git/index +0 -0
- .git/info/exclude +0 -6
- .git/logs/HEAD +0 -56
- .git/logs/refs/heads/ISS14 +0 -3
- .git/logs/refs/heads/PR5 +0 -3
- .git/logs/refs/heads/distr +0 -3
- .git/logs/refs/heads/github_workflow +0 -11
- .git/logs/refs/heads/main +0 -6
- .git/logs/refs/heads/pr10 +0 -3
- .git/logs/refs/heads/pr11 +0 -1
- .git/logs/refs/heads/pr12 +0 -2
- .git/logs/refs/heads/pr13 +0 -3
- .git/logs/refs/heads/pr14 +0 -2
- .git/logs/refs/heads/pr15 +0 -2
- .git/logs/refs/heads/pr2 +0 -2
- .git/logs/refs/heads/pr3 +0 -2
- .git/logs/refs/heads/pr4 +0 -2
- .git/logs/refs/heads/pr6 +0 -3
- .git/logs/refs/heads/pr8 +0 -2
- .git/logs/refs/heads/pr9 +0 -3
- .git/logs/refs/remotes/origin/HEAD +0 -1
- .git/logs/refs/remotes/origin/ISS14 +0 -2
- .git/logs/refs/remotes/origin/PR5 +0 -2
- .git/logs/refs/remotes/origin/distr +0 -2
- .git/logs/refs/remotes/origin/github_workflow +0 -10
- .git/logs/refs/remotes/origin/main +0 -5
- .git/logs/refs/remotes/origin/pr10 +0 -2
- .git/logs/refs/remotes/origin/pr12 +0 -1
- .git/logs/refs/remotes/origin/pr13 +0 -2
- .git/logs/refs/remotes/origin/pr14 +0 -2
- .git/logs/refs/remotes/origin/pr15 +0 -1
- .git/logs/refs/remotes/origin/pr2 +0 -1
- .git/logs/refs/remotes/origin/pr3 +0 -1
- .git/logs/refs/remotes/origin/pr4 +0 -1
- .git/logs/refs/remotes/origin/pr6 +0 -2
- .git/logs/refs/remotes/origin/pr8 +0 -1
- .git/logs/refs/remotes/origin/pr9 +0 -2
- .git/objects/00/e15c53e2190ca590a9fbe6c9b292178568cef8 +0 -0
- .git/objects/01/53ccf25d39e1ede671aa7b8f923d9e41890237 +0 -0
- .git/objects/01/e234feef599ebafee61c7a0a8685db305d6ccc +0 -0
- .git/objects/03/50c4a3b1b2a53eb2407badf3190c9093aef7f7 +0 -0
- .git/objects/03/7585e81567a46b86bdb12cacb8fe7a6b26cc0d +0 -0
- .git/objects/07/10acf175ac4d747e6d387e2813700f078f32be +0 -0
- .git/objects/07/711175e105bcf158fd31e720d28cc69b7e34d5 +0 -0
- .git/objects/09/65927d61c531e119991f121c1a19d578c65b3d +0 -0
- .git/objects/0a/096ef88c1aa81728774dd1c7e8c79bcf5c491e +0 -6
- .git/objects/0a/f4884b0f660b9e10ad51aaaee68c563ee83a73 +0 -0
- .git/objects/0c/96607ea9c9b2e4678f94da89aedea2bcd94cda +0 -0
- .git/objects/0d/cc4204ea2df65333510becfbc85182f113d33e +0 -0
- .git/objects/10/427d29d8b95bbb09784f6241002fa117d3dd0f +0 -0
- .git/objects/10/5ce2da2d6447d11dfe32bfb846c3d5b199fc99 +0 -0
- .git/objects/13/566b81b018ad684f3a35fee301741b2734c8f4 +0 -2
- .git/objects/15/8c2f20a3f3423f7675f4816add1429a0223266 +0 -0
- .git/objects/17/fb2589bea7ec4f87bdb4c9a1481d09594656ac +0 -0
- .git/objects/1d/3278e0f366dcec4cd5e98a62ccf104f28f265d +0 -0
- .git/objects/1d/ceebc9439f6cba980508f103677a80f97a672a +0 -0
- .git/objects/1f/a58087175ca3cf46c3348482595c9783426471 +0 -0
- .git/objects/1f/b285986eb7755c6345085c6f3289c750adb2c1 +0 -0
- .git/objects/23/8df90734a6bdbe960bd970867a023f88664892 +0 -0
- .git/objects/24/af438231a0bedf68e2070aa46b2faab13c2af6 +0 -0
- .git/objects/24/e98633b9e4a31fd210c2eeae5c8987e2b1f57c +0 -0
- .git/objects/26/38390d41e532e1d942b6d36145cea208b8289b +0 -0
- .git/objects/28/47dd9f4c763d4f58fa09f95fce005822588792 +0 -0
- .git/objects/29/2aa49c25b414b74ef20340586f6fde4c4154ef +0 -0
- .git/objects/2a/ecfb9b33d51b09b0b79a53b00d89accf6023c8 +0 -0
- .git/objects/30/0e2fe4548b0675c0443ccb9e273f058d91552b +0 -0
- .git/objects/3c/da27cf7a62a6163b038c5c8bda73482d563af7 +0 -0
- .git/objects/3e/7449d6da3916718af8e8d3bc0576d35db991cc +0 -0
- .git/objects/3e/cd291b85d01d6260251009567cbf306098cc63 +0 -0
- .git/objects/40/63b9f1b2483d0cfcdb2a7fa1e2aa0f8e6b6a22 +0 -2
- .git/objects/43/8fe1376a06cab53e0bdf1629ad7533830ece54 +0 -0
- .git/objects/45/202f63795e9ece7313332d730d7359bf169048 +0 -0
- .git/objects/45/5f376dc5185586f8e5fed236f6547a9b95fe9f +0 -0
- .git/objects/46/52c55db06f32df93e1ca0d1f6b90067a4d50ff +0 -0
- .git/objects/47/d311ab16718476b46780b3c0b078cc2f0b5363 +0 -0
- .git/objects/48/35139bc257d0ce1d0497836ea233fedf0cf7e2 +0 -0
- .git/objects/4a/298f060025643a78be0da2181ccb19b4cf1a52 +0 -0
- .git/objects/4a/44ccd200d7965ebfafa79e32f781d70bce8618 +0 -0
- .git/objects/4c/721f452dadf9e303017cdc6e544e721dfb922d +0 -0
- .git/objects/4d/f12098552734b547892b5a3412149d90acf5df +0 -0
- .git/objects/4f/175ec45d22e03a80873da3b3a9547af0460d69 +0 -0
- .git/objects/4f/1ff6d693ee6adde85c30d4068b1898e2672e38 +0 -0
- .git/objects/4f/f337e4b4f256d3034a70d88887dc9c6c7caa02 +0 -0
- .git/objects/52/2e1c3f5f286ae06f58ab5cc2e6f796366914d9 +0 -0
- .git/objects/53/13510cdc184392e70393f3e99f0b99c9cf4b07 +0 -0
- .git/objects/53/847edbeebc9e1eb3e0bddcc0a726737aae2ff4 +0 -0
- .git/objects/54/78494c0fb874eb4112626912250e9d87b79782 +0 -0
- .git/objects/56/12275dd6ffd118605643ce5be0b09f82099ecf +0 -0
- .git/objects/61/41d3de631666d108a4cd10aa28f805e7ba2176 +0 -0
- .git/objects/61/a52a6564a3416abcfbde4f9c31ea003797be1c +0 -0
- .git/objects/62/9a884ee20ea72f3e529f09d17cf2c6bcdde59a +0 -0
- .git/objects/65/79867fd47b1f1d94d7f86b363290b9992dd3d5 +0 -0
- .git/objects/66/56027dc3dce02aeb380558b28a1155f9f56221 +0 -0
- .git/objects/66/d6fcea767d7fb1379a06649988656f59b2f11f +0 -0
- .git/objects/67/5a2f2488c4472fefe8c01d58af3036e8ea3646 +0 -0
- .git/objects/68/51843a6186387cc24e57013ecb48234c29d606 +0 -0
- .git/objects/68/83ffef5964a7e304ad31a36bc826b10b466630 +0 -0
- .git/objects/6b/fc4fdd895285d93151265e2a61a1f9b9bfeb23 +0 -0
- .git/objects/6c/0869f2a22f30604443d53c2193c5862f9631ab +0 -0
- .git/objects/6c/8c18a8b818bc289cdb5af64bf4d9c1965cd0bb +0 -2
- .git/objects/6d/05879d4c160a3f719b8a0e955ff0942d5c8cc5 +0 -0
- .git/objects/6d/165188630ed19e578141107a626e45266bde15 +0 -0
- .git/objects/6e/8321d8213769293fe28b366dceacdba4e1ae53 +0 -0
- .git/objects/6f/a39e12d18f8332b416a39022a17038b2fb2b79 +0 -0
- .git/objects/70/3dddb937f919e83e887796e5cde65a82e550b7 +0 -0
- .git/objects/70/bcd529437b15a83ecc1851515ceb5baee2c968 +0 -0
- .git/objects/71/2b09b10356caf10cdb2a164ac31f2fdca9e8df +0 -0
- .git/objects/72/17705dce1fb7641f1a6be9cad6a4c0c0bdef54 +0 -0
- .git/objects/72/2a2b31541c4b4eabd01df1cd9f78bf902b48f2 +0 -0
- .git/objects/74/0ee4f5f65869ebb2990d984a238c1b83bf2d2e +0 -0
- .git/objects/76/675f4e4a951b71830de8763b40f8dba5f0e329 +0 -0
- .git/objects/78/a0e7777e91f85a4304fd7b3ac57d1654496fae +0 -0
- .git/objects/7a/2351a44cd4ce936a35e2a9637da3626200abc7 +0 -0
- .git/objects/7b/cb5422cd40f863e38c1415e879c66b2b0f6ed0 +0 -0
- .git/objects/80/fa8421446fe3ef9facb790cf8e93e8bfa7bf86 +0 -0
- .git/objects/81/2ef37d3eaf8b52e4ffc3ad429fa230780b3d3e +0 -0
- .git/objects/82/d7fdd2beb79b6b64ab2da1d791d3a2b64d583f +0 -1
- .git/objects/87/d4c9ebcc955f31a4e125fb692e8f6b7e2e9004 +0 -0
- .git/objects/87/edbd2d1f2dbdfb5d64c9dcfb17cb330143aa7a +0 -0
- .git/objects/89/57f44e305c6c4cd64d0e8f4c6677f834cb810c +0 -2
- .git/objects/89/8a90dbe5babc16e488c790849f1d189814aacc +0 -0
- .git/objects/8c/2fccdebc495f29cbb18b641815020290b6d531 +0 -0
- .git/objects/8f/7de669deb74e7fb028bd0d8661d3ab19902ef2 +0 -0
- .git/objects/90/70debb5147ecc0efc58f48c3f5d1afa0a6d058 +0 -0
- .git/objects/91/af3b7f9fc304aa4d5593ab2dea81edd16c6842 +0 -0
- .git/objects/91/e75ebdffb1bd2868362c83269ac8757511a113 +0 -0
- .git/objects/92/cc82343d809b40274fc896aa639bcc6cdeef53 +0 -0
- .git/objects/94/a25f7f4cb416c083d265558da75d457237d671 +0 -0
- .git/objects/98/e9c322ec3ddf6150bdc7b6eab43d50dc68477f +0 -0
- .git/objects/99/d26651a87295a4967a9c662b4fc483dca54303 +0 -0
- .git/objects/99/f64fe8661dde8511c7581136ff1343df72ae11 +0 -0
- .git/objects/9c/0b1c3e999e338805b480e135fe7b2f0300f534 +0 -0
- .git/objects/9e/1b178c51649d878eeb1b99063594ed80903ce9 +0 -0
- .git/objects/a1/6305ecc5eeba77cae9e067f8491936ce9852f9 +0 -0
- .git/objects/a2/059f9bac3f3fdc5f58fa93ef828b03496807e3 +0 -0
- .git/objects/a2/6aa4fc4c3378ef756103f26ceb3618d2615ddb +0 -0
- .git/objects/a2/b3fd2fcf75b4e01fc46313f5b6b10d9574ea45 +0 -0
- .git/objects/a8/c116e2796299d870623c7c7bade3a482ced1e1 +0 -0
- .git/objects/a9/6353914fff11500d5e307c6a39379a7dc3f3a5 +0 -0
- .git/objects/ab/0e7004a39aedeffecd15a29948ef2983f8fedb +0 -0
- .git/objects/ab/2336f2f5c66697cfeff071f874c5171dc3e203 +0 -0
- .git/objects/ab/2e84e0513674a3feb40a625e67c826830cf955 +0 -0
- .git/objects/ae/098f9246f2cec2ef7487ab710dbb2ec4ba6f25 +0 -0
- .git/objects/ae/4da2d6bc50f83d660c0fe9839a280487f7ef87 +0 -0
- .git/objects/ae/7dc4cf2e4e4d808e632ce118b0d317e7e6d06b +0 -0
- .git/objects/ae/9c9580e50bff0e005fdf99e0a5a9ff6e76b91c +0 -0
- .git/objects/ae/b009618d2b8fb2c50ca54ea0a3657ffdb6c4ba +0 -0
- .git/objects/b3/24de33b5a21a01c9ae8b33636d5f1aca3efd31 +0 -0
- .git/objects/b5/320acd6bb7f62a174eccefe2ddc3eb1eff24e9 +0 -0
- .git/objects/b5/6828ec56fa75308c2f6c9db08ec414fd0b566d +0 -0
- .git/objects/b6/2c268663d053e57692cd9bf21f6c0c61a53411 +0 -0
- .git/objects/b8/1d5264f5b7448228120be9283b50ca1797bd02 +0 -0
- .git/objects/b9/210e2bbaff2e3b546ca3c65fc9d42fecef0f6b +0 -1
- .git/objects/bb/ab42af439de66ac98d4024416c8b32b3066e45 +0 -0
- .git/objects/bc/2ece3504ba0d8e65a40f223c8d04fc07b41fc7 +0 -0
- .git/objects/c0/3e8fe2975c8a6fc281915546747c07905a81a7 +0 -0
- .git/objects/c0/bdda06278a78d10a16e7df4dce2c73387c91be +0 -0
- .git/objects/c1/0d3d0df470652770af295bbeb9848e76daea05 +0 -0
- .git/objects/c2/574881c5be951d33c8df083325c241c6c83c55 +0 -0
- .git/objects/c2/d6ec54c49f9b6141f28e3b9b0276d7c71d18d2 +0 -0
- .git/objects/c2/f2fb023dbbe83b13b3a08bbf9eb7ee3539ae17 +0 -2
- .git/objects/c5/a8cf53296097c73cb02ed9b2e1aefb3ff4ccdc +0 -0
- .git/objects/c8/8a8dadb163234e72ffd8e29565200c537a0bc0 +0 -3
- .git/objects/c8/c228603f594ff421808257b643cc3a37aff7d3 +0 -0
- .git/objects/cb/3f5041c4acf866cff013ea916d12dc2482fe35 +0 -0
- .git/objects/cc/91ad25539ed2c756aa3de1b07cacece6ec0137 +0 -0
- .git/objects/d1/49cd9c1d61f461a7c0d6023ae9028f0ee525a9 +0 -0
- .git/objects/d5/292a1122dd0382ddb15b1c589bfe2370dd9062 +0 -0
- .git/objects/d5/5e55c84565f30bca7b4ef17551b9362a752d7f +0 -0
- .git/objects/d6/3d373dafc2d3cc9ea97033e0a05d90185118f5 +0 -0
- .git/objects/d6/c28b29f40cca12d8e80c02e5539e0ccdb5e69e +0 -0
- .git/objects/d7/238aa2b4808051f8d09b263c5c24d0921d1509 +0 -1
- .git/objects/d7/6b40fe82c1b34d7f1eadb11a4919aed78b965b +0 -0
- .git/objects/d8/1ab4149f5cf0ed537300961b50b741117d5d0d +0 -0
- .git/objects/d8/4dc6f797e9140db9540ca9de0fdef62238620c +0 -0
- .git/objects/d8/b3f6cbf0c04e3481a87fcd0d588f490d9fdac7 +0 -0
- .git/objects/db/dfb17058375783a115dec9b8667166daeafea7 +0 -0
- .git/objects/db/ef36011c0e082be9e83554b12b2e6ce5449890 +0 -0
- .git/objects/dc/26d426cf68c8b82ca3aeda0e7656b9c63a2a17 +0 -0
- .git/objects/e0/3c1b89624ed6163c875c84c114ce87ffb33e9f +0 -0
- .git/objects/e1/801f544ddfb099ec8b7ffea3d7d0b6226f07cd +0 -0
- .git/objects/e3/cf7976790f02529fae084d3d698133dfdf33cb +0 -0
- .git/objects/e5/c4125bfafa8ca31149ef216e5188a82cdfd9bc +0 -0
- .git/objects/e6/0fd8a2ed55b272863b70676def2feb34a2f5ab +0 -0
- .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
- .git/objects/e9/b540e3f95a3bddb31f9c7154ddc8d1416a5a49 +0 -0
- .git/objects/ea/1df7109d325e658658d32745c75adc6fab59de +0 -0
- .git/objects/eb/6c739b8f3e5b04d92d961f586cab2e6aed9299 +0 -0
- .git/objects/ed/2445f60de723b9c2f677f04890f42005b7cf75 +0 -0
- .git/objects/ef/3861fba8550a65b4bdc9705c7a9ae6c68f7811 +0 -0
- .git/objects/f2/8d82d529c1cd6185b393d2e779364550f7ece4 +0 -0
- .git/objects/f3/872867a1dbe7db3eb03d5e3817b65276260bb9 +0 -0
- .git/objects/f3/c2f22db356e7aaf2700677b764eb9c44abce8b +0 -0
- .git/objects/f7/b8019fae044e97766b5046d48b792e56263d9f +0 -2
- .git/objects/f8/28fe560a37bfee17a4e3af48a1ac2849e7bd47 +0 -0
- .git/objects/f9/099a73a609541d9d411193d2fa975852080cdf +0 -0
- .git/objects/f9/a1fcc542a53abc04564661c174bf85015ae4c9 +0 -2
- .git/objects/fb/bb61d4888712eae9a07c93820ee6dcfbf068db +0 -0
- .git/objects/fb/cbd444bb66d87e9e116dfe42331e4f9cff1b5a +0 -0
- .git/objects/ff/2f55840120e843740fab47d50dcdc7cedef91d +0 -0
- .git/objects/ff/ba5c4ae674142c7fdace47e34357ba8504ba58 +0 -0
- .git/objects/pack/pack-286afde1b841483a0d40e4c4d4bc844eb78906e1.idx +0 -0
- .git/objects/pack/pack-286afde1b841483a0d40e4c4d4bc844eb78906e1.pack +0 -0
- .git/objects/pack/pack-286afde1b841483a0d40e4c4d4bc844eb78906e1.rev +0 -0
- .git/packed-refs +0 -2
- .git/refs/heads/ISS14 +0 -1
- .git/refs/heads/PR5 +0 -1
- .git/refs/heads/distr +0 -1
- .git/refs/heads/github_workflow +0 -1
- .git/refs/heads/main +0 -1
- .git/refs/heads/pr10 +0 -1
- .git/refs/heads/pr11 +0 -1
- .git/refs/heads/pr12 +0 -1
- .git/refs/heads/pr13 +0 -1
- .git/refs/heads/pr14 +0 -1
- .git/refs/heads/pr15 +0 -1
- .git/refs/heads/pr2 +0 -1
- .git/refs/heads/pr3 +0 -1
- .git/refs/heads/pr4 +0 -1
- .git/refs/heads/pr6 +0 -1
- .git/refs/heads/pr8 +0 -1
- .git/refs/heads/pr9 +0 -1
- .git/refs/remotes/origin/HEAD +0 -1
- .git/refs/remotes/origin/ISS14 +0 -1
- .git/refs/remotes/origin/PR5 +0 -1
- .git/refs/remotes/origin/distr +0 -1
- .git/refs/remotes/origin/github_workflow +0 -1
- .git/refs/remotes/origin/main +0 -1
- .git/refs/remotes/origin/pr10 +0 -1
- .git/refs/remotes/origin/pr12 +0 -1
- .git/refs/remotes/origin/pr13 +0 -1
- .git/refs/remotes/origin/pr14 +0 -1
- .git/refs/remotes/origin/pr15 +0 -1
- .git/refs/remotes/origin/pr2 +0 -1
- .git/refs/remotes/origin/pr3 +0 -1
- .git/refs/remotes/origin/pr4 +0 -1
- .git/refs/remotes/origin/pr6 +0 -1
- .git/refs/remotes/origin/pr8 +0 -1
- .git/refs/remotes/origin/pr9 +0 -1
- .github/workflows/code-style.yml +0 -16
- .github/workflows/cr.yml +0 -99
- .gitignore +0 -178
- CONTRIBUTING.md +0 -44
- Makefile +0 -23
- README.md +0 -6
- ai_cr-0.1.0.dist-info/LICENSE +0 -21
- ai_cr-0.1.0.dist-info/METADATA +0 -32
- ai_cr-0.1.0.dist-info/RECORD +0 -277
- ai_cr-0.1.0.dist-info/entry_points.txt +0 -3
- cr-github-workflow-example.yml +0 -100
- cr-prompt.j2 +0 -18
- cr.py +0 -71
- publish.py +0 -16
- pyproject.toml +0 -57
- requirements.txt +0 -3
- /LICENSE → /ai_cr-0.4.3.dist-info/LICENSE +0 -0
- {ai_cr-0.1.0.dist-info → ai_cr-0.4.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,305 @@
|
|
1
|
+
report_template_md = """
|
2
|
+
# 🤖 I've Reviewed the Code
|
3
|
+
|
4
|
+
{% if report.summary -%}
|
5
|
+
{{ report.summary }}
|
6
|
+
{%- endif %}
|
7
|
+
|
8
|
+
**{%- if report.total_issues > 0 %}⚠️{% endif -%}
|
9
|
+
Total issues: `{{ report.total_issues }}`
|
10
|
+
{{- ' ' -}}
|
11
|
+
in `{{ report.number_of_processed_files }}` files**
|
12
|
+
|
13
|
+
{%- for issue in report.plain_issues -%}
|
14
|
+
{{"\n"}}## `#{{ issue.id}}` {{ issue.title -}}
|
15
|
+
{{ "\n"}}[{{ issue.file }}{{' '}}
|
16
|
+
|
17
|
+
{%- if issue.affected_lines -%}
|
18
|
+
{%- for i in issue.affected_lines -%}
|
19
|
+
L{{ i.start_line }}{%- if i.end_line != i.start_line -%}-L{{ i.end_line }}{%- endif -%}
|
20
|
+
{%- if loop.last == false -%}, {%- endif -%}
|
21
|
+
{%- endfor -%}
|
22
|
+
{%- endif -%}
|
23
|
+
]({{ issue.github_code_link(github_env) }})
|
24
|
+
|
25
|
+
{{"\n"}}{{ issue.details -}}
|
26
|
+
{{"\n"}}**Tags: {{ ', '.join(issue.tags) }}**
|
27
|
+
{%- for i in issue.affected_lines -%}
|
28
|
+
{%- if i.affected_code %}\n**Affected code:**\n```{{ i.syntax_hint }}\n{{ i.affected_code }}\n```{%- endif -%}
|
29
|
+
{%- if i.proposal %}\n**Proposed change:**\n```{{ i.syntax_hint }}\n{{ i.proposal }}\n```{%- endif -%}
|
30
|
+
{%- endfor -%}
|
31
|
+
{{ "\n" }}
|
32
|
+
{%- endfor -%}
|
33
|
+
|
34
|
+
"""
|
35
|
+
retries = 3
|
36
|
+
prompt = """
|
37
|
+
{{ self_id }}
|
38
|
+
----TASK----
|
39
|
+
Review the provided code diff carefully and identify *only* highly confident issues which are relevant to any code context.
|
40
|
+
|
41
|
+
----CODE----
|
42
|
+
{{ input }}
|
43
|
+
--------
|
44
|
+
|
45
|
+
{% if file_lines -%}
|
46
|
+
----ADDITIONAL CONTEXT----
|
47
|
+
{{ file_lines }}
|
48
|
+
{%- endif %}
|
49
|
+
|
50
|
+
----TASK GUIDELINES----
|
51
|
+
- Only report issues you are **100% confident** are relevant to any context.
|
52
|
+
- Only include issues that are **significantly valuable** to the maintainers (e.g., bugs, security flaws, or clear maintainability concerns).
|
53
|
+
- Do **not** report vague, theoretical, or overly generic advice.
|
54
|
+
- Do **not** report anything with medium or lower confidence.
|
55
|
+
{{ json_requirements }}
|
56
|
+
|
57
|
+
Respond with a valid JSON array of issues in the following format:
|
58
|
+
[
|
59
|
+
{
|
60
|
+
"title": "<issue_title>",
|
61
|
+
"details": "<issue_description>",
|
62
|
+
"tags": ["<issue_tag1>", "<issue_tag2>"],
|
63
|
+
"severity": <issue_severity>,
|
64
|
+
"confidence": <confidence_score>
|
65
|
+
"affected_lines": [ // optional; list of affected lines
|
66
|
+
{
|
67
|
+
"start_line": <start_line:int>,
|
68
|
+
"end_line": <end_line:int>,
|
69
|
+
"proposal": "<proposed code to replace the affected lines (optional)>"
|
70
|
+
},
|
71
|
+
...
|
72
|
+
]
|
73
|
+
},
|
74
|
+
...
|
75
|
+
]
|
76
|
+
Available issue tags:
|
77
|
+
- bug
|
78
|
+
- security
|
79
|
+
- performance
|
80
|
+
- readability
|
81
|
+
- maintainability
|
82
|
+
- overcomplexity
|
83
|
+
- language
|
84
|
+
- architecture
|
85
|
+
- compatibility
|
86
|
+
- deprecation
|
87
|
+
- anti-pattern
|
88
|
+
- naming
|
89
|
+
- code-style
|
90
|
+
|
91
|
+
Issue severity scale:
|
92
|
+
- 1 — Critical
|
93
|
+
- 2 — Major
|
94
|
+
- 3 — Minor
|
95
|
+
- 4 — Trivial
|
96
|
+
- 5 — Suggestion
|
97
|
+
|
98
|
+
Confidence scale:
|
99
|
+
- 1 — Highest, 100% confidence that code requires changes in any context
|
100
|
+
- 2 — Very High
|
101
|
+
- 3 — High
|
102
|
+
- 4 — Medium - Should not be reported
|
103
|
+
|
104
|
+
(!) - If no issues found according to the criterias, respond with empty list: []
|
105
|
+
"""
|
106
|
+
# Remove issues with confidence + severity > 3
|
107
|
+
post_process = """
|
108
|
+
for fn in issues:
|
109
|
+
issues[fn] = [
|
110
|
+
i for i in issues[fn]
|
111
|
+
if i["confidence"] == 1 and i["severity"] <= 2
|
112
|
+
]
|
113
|
+
"""
|
114
|
+
summary_prompt = """
|
115
|
+
{{ self_id }}
|
116
|
+
Summarize the code review in one sentence.
|
117
|
+
--Reviewed Changes--
|
118
|
+
{% for part in diff %}{{ part }}\n{% endfor %}
|
119
|
+
--Detected Issues--
|
120
|
+
{{ issues | tojson(indent=2) }}
|
121
|
+
---
|
122
|
+
If code changs contains exceptional achievements, you may additionally present the award in summary text.
|
123
|
+
--Available Awards--
|
124
|
+
{{ awards }}
|
125
|
+
---
|
126
|
+
Your response will be parsed programmatically, so do not include any additional text.
|
127
|
+
Use Markdown formatting in your response.
|
128
|
+
"""
|
129
|
+
|
130
|
+
[prompt_vars]
|
131
|
+
self_id = """
|
132
|
+
You are a subsystem of an AI-powered software platform, specifically tasked with performing expert code reviews.
|
133
|
+
Act as a senior, highly experienced software engineer.
|
134
|
+
"""
|
135
|
+
json_requirements = """
|
136
|
+
- ⚠️ IMPORTANT: RESPOND ONLY WITH VALID JSON, YOUR RESPONSE WILL BE PARSED PROGRAMMATICALLY.
|
137
|
+
- Do not include any additional text or explanation outside the specified format.
|
138
|
+
"""
|
139
|
+
awards = """
|
140
|
+
## 🧙♂️ "Refactoring Archmage"
|
141
|
+
**For:** Elegantly transforming complex code into simple code without losing functionality.
|
142
|
+
|
143
|
+
**Presentation example:**
|
144
|
+
```
|
145
|
+
🧙♂️ REFACTORING ARCHMAGE 🧙♂️
|
146
|
+
"You transformed 47 lines of chaotic code into 12 lines of crystal clarity.
|
147
|
+
Like Gandalf transforming from Grey to White, this code now radiates
|
148
|
+
light instead of confusion. The coding magic school gives a standing ovation."
|
149
|
+
```
|
150
|
+
|
151
|
+
## 🕰️ "Time Machine"
|
152
|
+
**For:** Code that prevents future problems others haven't noticed yet.
|
153
|
+
|
154
|
+
**Presentation example:**
|
155
|
+
```
|
156
|
+
🕰️ TIME MACHINE 🕰️
|
157
|
+
"Your edge case handler just saved the company from a dark
|
158
|
+
alternative timeline where at 3:00 AM next month
|
159
|
+
the DevOps team goes crazy from incomprehensible errors. History has changed,
|
160
|
+
the future is no longer what it was."
|
161
|
+
```
|
162
|
+
|
163
|
+
## 🎭 "Shakespearean Playwright"
|
164
|
+
**For:** Exceptionally expressive variable and function names that tell a story.
|
165
|
+
|
166
|
+
**Presentation example:**
|
167
|
+
```
|
168
|
+
🎭 SHAKESPEAREAN PLAYWRIGHT 🎭
|
169
|
+
"'processUserInputAndValidateBeforeSending' — a whole act of drama in one
|
170
|
+
function name! Such clarity of intent, such drama! The entire code is a stage,
|
171
|
+
and your variables are actors with clearly defined roles. The audience is thrilled."
|
172
|
+
```
|
173
|
+
|
174
|
+
## 🧩 "Puzzle Master"
|
175
|
+
**For:** Solving a complex logical problem in a particularly creative way.
|
176
|
+
|
177
|
+
**Presentation example:**
|
178
|
+
```
|
179
|
+
🧩 PUZZLE MASTER 🧩
|
180
|
+
"Where others saw impassable thickets of conditions, you paved an elegant algorithmic
|
181
|
+
path. Your solution looks so natural that now it seems like there could never have been
|
182
|
+
another way. Rubik applauds."
|
183
|
+
```
|
184
|
+
|
185
|
+
## 🐛 "Ghostbuster"
|
186
|
+
**For:** Detecting and fixing elusive bugs or potential issues.
|
187
|
+
|
188
|
+
**Presentation example:**
|
189
|
+
```
|
190
|
+
🐛 GHOSTBUSTER 🐛
|
191
|
+
"This elusive bug was hiding in the shadows for five sprints, feeding on developers'
|
192
|
+
souls and sowing chaos. 'Who are you?' it screamed when you dragged it into the light
|
193
|
+
with your precise fix. Paranormal activity eliminated."
|
194
|
+
```
|
195
|
+
|
196
|
+
## 🏛️ "Architectural Virtuoso"
|
197
|
+
**For:** Code structuring that promotes extensibility and flexibility.
|
198
|
+
|
199
|
+
**Presentation example:**
|
200
|
+
```
|
201
|
+
🏛️ ARCHITECTURAL VIRTUOSO 🏛️
|
202
|
+
"Your architecture is like the Parthenon of modern code: proportional, harmonious, and seems
|
203
|
+
to withstand the pressure of time and changing requirements. Vitruvius records your patterns
|
204
|
+
for future generations."
|
205
|
+
```
|
206
|
+
|
207
|
+
## 🧬 "Code Geneticist"
|
208
|
+
**For:** Successful use of inheritance/composition or other complex OOP concepts.
|
209
|
+
|
210
|
+
**Presentation example:**
|
211
|
+
```
|
212
|
+
🧬 CODE GENETICIST 🧬
|
213
|
+
"Your elegant inheritance chain has mutated the code into a new life form — more
|
214
|
+
adaptive, more evolutionarily stable. Natural selection kindly approved these changes,
|
215
|
+
while unacceptable complexity remains in the paleontological past of development."
|
216
|
+
```
|
217
|
+
|
218
|
+
## 🔄 "Zen of Loops"
|
219
|
+
**For:** Writing particularly efficient and understandable loops/iterations.
|
220
|
+
|
221
|
+
**Presentation example:**
|
222
|
+
```
|
223
|
+
🔄 ZEN OF LOOPS 🔄
|
224
|
+
"Your loop impresses with its laconic wisdom. Nothing extra, nothing forgotten,
|
225
|
+
perfect balance between readability and performance. 'Before writing a loop,
|
226
|
+
think whether it's needed at all,' says the master. Your loop — is needed."
|
227
|
+
```
|
228
|
+
|
229
|
+
## 🛡️ "Gate Guardian"
|
230
|
+
**For:** Excellent input validation and protection against edge cases.
|
231
|
+
|
232
|
+
**Presentation example:**
|
233
|
+
```
|
234
|
+
🛡️ GATE GUARDIAN 🛡️
|
235
|
+
"No bad data shall pass your vigilant defense. Users may enter
|
236
|
+
the most bizarre combinations, but your code stands firm, like a sentinel at the gates
|
237
|
+
of the data city. 'You shall not pass!' it speaks to invalid format."
|
238
|
+
```
|
239
|
+
|
240
|
+
## 🎨 "Readability Impressionist"
|
241
|
+
**For:** Code that reads like well-written prose.
|
242
|
+
|
243
|
+
**Presentation example:**
|
244
|
+
```
|
245
|
+
🎨 READABILITY IMPRESSIONIST 🎨
|
246
|
+
"Reading your code, you feel sunlight falling on the water lilies of clarity,
|
247
|
+
like a breeze playing in the willows of logic. Each line is a brush stroke,
|
248
|
+
and together they create a picture that can be understood at first glance."
|
249
|
+
```
|
250
|
+
|
251
|
+
## 🚀 "Optimization Pioneer"
|
252
|
+
**For:** Significant performance improvement without sacrificing readability.
|
253
|
+
|
254
|
+
**Presentation example:**
|
255
|
+
```
|
256
|
+
🚀 OPTIMIZATION PIONEER 🚀
|
257
|
+
"Oh! Your algorithm now flies at the speed of light! If it used to crawl
|
258
|
+
like a snail through O(n²) sand, now it races down the O(log n) highway.
|
259
|
+
The passengers of this code won't even notice how they arrive at their destination!"
|
260
|
+
```
|
261
|
+
|
262
|
+
## 📚 "Code Chronicler"
|
263
|
+
**For:** Exceptionally useful and informative comments.
|
264
|
+
|
265
|
+
**Presentation example:**
|
266
|
+
```
|
267
|
+
📚 CODE CHRONICLER 📚
|
268
|
+
"Your comments are like an ancient manuscript revealing the secrets of forgotten civilizations.
|
269
|
+
'And there was light,' you said, and indeed the light bulb of understanding lit up above the heads
|
270
|
+
of all who will read this code in the future."
|
271
|
+
```
|
272
|
+
|
273
|
+
## 🧪 "Testing Alchemist"
|
274
|
+
**For:** Writing particularly creative and thorough tests.
|
275
|
+
|
276
|
+
**Presentation example:**
|
277
|
+
```
|
278
|
+
🧪 TESTING ALCHEMIST 🧪
|
279
|
+
"In your testing crucible you mixed reagents of edge cases, catalyst
|
280
|
+
of boundary conditions and essence of unit tests. The philosopher's stone of quality was born —
|
281
|
+
your code is now immortal in the face of regression!"
|
282
|
+
```
|
283
|
+
|
284
|
+
## 🗿 "Ancient Artifact Decoder"
|
285
|
+
**For:** Successfully working with complex legacy code.
|
286
|
+
|
287
|
+
**Presentation example:**
|
288
|
+
```
|
289
|
+
🗿 ANCIENT ARTIFACT DECODER 🗿
|
290
|
+
"You stand among the ruins of code written five years ago in the forgotten language of the ancients.
|
291
|
+
Like Champollion with the Rosetta Stone, you deciphered the hieroglyphs of functions,
|
292
|
+
restored lost knowledge and now bestow it upon a new generation."
|
293
|
+
```
|
294
|
+
|
295
|
+
## 🎵 "Pattern Composer"
|
296
|
+
**For:** Sophisticated application or combination of multiple design patterns.
|
297
|
+
|
298
|
+
**Presentation example:**
|
299
|
+
```
|
300
|
+
🎵 PATTERN COMPOSER 🎵
|
301
|
+
"Your symphony of patterns sounds magnificent! Factory method opens the first movement,
|
302
|
+
decorators add depth and texture, and observer masterfully completes the composition.
|
303
|
+
The Gang of Four gives a standing ovation from the stalls."
|
304
|
+
```
|
305
|
+
"""
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import logging
|
2
|
+
from datetime import datetime
|
3
|
+
|
4
|
+
import microcore as mc
|
5
|
+
|
6
|
+
from .constants import ENV_CONFIG_FILE
|
7
|
+
|
8
|
+
|
9
|
+
def setup_logging():
|
10
|
+
class CustomFormatter(logging.Formatter):
|
11
|
+
def format(self, record):
|
12
|
+
dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
|
13
|
+
message, level_name = record.getMessage(), record.levelname
|
14
|
+
if record.levelno == logging.WARNING:
|
15
|
+
message = mc.ui.yellow(message)
|
16
|
+
level_name = mc.ui.yellow(level_name)
|
17
|
+
if record.levelno >= logging.ERROR:
|
18
|
+
message = mc.ui.red(message)
|
19
|
+
level_name = mc.ui.red(level_name)
|
20
|
+
return f"{dt} {level_name}: {message}"
|
21
|
+
|
22
|
+
handler = logging.StreamHandler()
|
23
|
+
handler.setFormatter(CustomFormatter())
|
24
|
+
logging.basicConfig(level=logging.INFO, handlers=[handler])
|
25
|
+
|
26
|
+
|
27
|
+
def bootstrap():
|
28
|
+
"""Bootstrap the application with the environment configuration."""
|
29
|
+
setup_logging()
|
30
|
+
logging.info("Bootstrapping...")
|
31
|
+
mc.configure(
|
32
|
+
DOT_ENV_FILE=ENV_CONFIG_FILE,
|
33
|
+
VALIDATE_CONFIG=False,
|
34
|
+
USE_LOGGING=True,
|
35
|
+
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
36
|
+
)
|
37
|
+
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [100, 15]
|
ai_code_review/cli.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import sys
|
4
|
+
import os
|
5
|
+
import shutil
|
6
|
+
|
7
|
+
import microcore as mc
|
8
|
+
import async_typer
|
9
|
+
import typer
|
10
|
+
from .core import review
|
11
|
+
from .report_struct import Report
|
12
|
+
from git import Repo
|
13
|
+
import requests
|
14
|
+
|
15
|
+
from .constants import ENV_CONFIG_FILE
|
16
|
+
from .bootstrap import bootstrap
|
17
|
+
from .project_config import ProjectConfig
|
18
|
+
|
19
|
+
app = async_typer.AsyncTyper(
|
20
|
+
pretty_exceptions_show_locals=False,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
if sys.platform == "win32":
|
25
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
26
|
+
|
27
|
+
|
28
|
+
@app.callback(invoke_without_command=True)
|
29
|
+
def cli(ctx: typer.Context, filters=typer.Option("", "--filter", "-f", "--filters")):
|
30
|
+
if ctx.invoked_subcommand != "setup":
|
31
|
+
bootstrap()
|
32
|
+
if not ctx.invoked_subcommand:
|
33
|
+
asyncio.run(review(filters=filters))
|
34
|
+
|
35
|
+
|
36
|
+
@app.async_command(help="Configure LLM for local usage interactively")
|
37
|
+
async def setup():
|
38
|
+
mc.interactive_setup(ENV_CONFIG_FILE)
|
39
|
+
|
40
|
+
|
41
|
+
@app.async_command()
|
42
|
+
async def render(format: str = Report.Format.MARKDOWN):
|
43
|
+
print(Report.load().render(format=format))
|
44
|
+
|
45
|
+
|
46
|
+
@app.async_command(help="Review remote code")
|
47
|
+
async def remote(url=typer.Option(), branch=typer.Option()):
|
48
|
+
if os.path.exists("reviewed-repo"):
|
49
|
+
shutil.rmtree("reviewed-repo")
|
50
|
+
Repo.clone_from(url, branch=branch, to_path="reviewed-repo")
|
51
|
+
prev_dir = os.getcwd()
|
52
|
+
try:
|
53
|
+
os.chdir("reviewed-repo")
|
54
|
+
await review()
|
55
|
+
finally:
|
56
|
+
os.chdir(prev_dir)
|
57
|
+
|
58
|
+
|
59
|
+
@app.async_command(help="Leave a GitHub PR comment with the review.")
|
60
|
+
async def github_comment(
|
61
|
+
token: str = typer.Option(
|
62
|
+
os.environ.get("GITHUB_TOKEN", ""), help="GitHub token (or set GITHUB_TOKEN env var)"
|
63
|
+
),
|
64
|
+
):
|
65
|
+
"""
|
66
|
+
Leaves a comment with the review on the current GitHub pull request.
|
67
|
+
"""
|
68
|
+
file = "code-review-report.txt"
|
69
|
+
if not os.path.exists(file):
|
70
|
+
print(f"Review file not found: {file}")
|
71
|
+
raise typer.Exit(4)
|
72
|
+
|
73
|
+
with open(file, "r", encoding="utf-8") as f:
|
74
|
+
body = f.read()
|
75
|
+
|
76
|
+
if not token:
|
77
|
+
print("GitHub token is required (--token or GITHUB_TOKEN env var).")
|
78
|
+
raise typer.Exit(1)
|
79
|
+
|
80
|
+
github_env = ProjectConfig.load().prompt_vars["github_env"]
|
81
|
+
repo = github_env.get("github_repo", "")
|
82
|
+
pr_env_val = github_env.get("github_pr_number", "")
|
83
|
+
logging.info(f"github_pr_number = {pr_env_val}")
|
84
|
+
|
85
|
+
# e.g. could be "refs/pull/123/merge" or a direct number
|
86
|
+
if "/" in pr_env_val and "pull" in pr_env_val:
|
87
|
+
# refs/pull/123/merge
|
88
|
+
try:
|
89
|
+
pr_num_candidate = pr_env_val.strip("/").split("/")
|
90
|
+
idx = pr_num_candidate.index("pull")
|
91
|
+
pr = int(pr_num_candidate[idx + 1])
|
92
|
+
except Exception:
|
93
|
+
pr = 0
|
94
|
+
else:
|
95
|
+
try:
|
96
|
+
pr = int(pr_env_val)
|
97
|
+
except Exception:
|
98
|
+
pr = 0
|
99
|
+
|
100
|
+
api_url = f"https://api.github.com/repos/{repo}/issues/{pr}/comments"
|
101
|
+
headers = {
|
102
|
+
"Authorization": f"token {token}",
|
103
|
+
"Accept": "application/vnd.github+json",
|
104
|
+
}
|
105
|
+
data = {"body": body}
|
106
|
+
|
107
|
+
resp = requests.post(api_url, headers=headers, json=data)
|
108
|
+
if 200 <= resp.status_code < 300:
|
109
|
+
logging.info(f"Posted review comment to PR #{pr} in {repo}")
|
110
|
+
else:
|
111
|
+
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
112
|
+
raise typer.Exit(5)
|
@@ -0,0 +1,7 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
|
4
|
+
PROJECT_CONFIG_FILE = Path(".ai-code-review.toml")
|
5
|
+
PROJECT_CONFIG_DEFAULTS_FILE = Path(__file__).resolve().parent / PROJECT_CONFIG_FILE
|
6
|
+
ENV_CONFIG_FILE = Path("~/.env.ai-code-review").expanduser()
|
7
|
+
JSON_REPORT_FILE_NAME = "code-review-report.json"
|
ai_code_review/core.py
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
import fnmatch
|
2
|
+
import logging
|
3
|
+
from typing import Iterable
|
4
|
+
|
5
|
+
import microcore as mc
|
6
|
+
from git import Repo
|
7
|
+
from unidiff import PatchSet, PatchedFile
|
8
|
+
from unidiff.constants import DEV_NULL
|
9
|
+
|
10
|
+
from .project_config import ProjectConfig
|
11
|
+
from .report_struct import Report
|
12
|
+
|
13
|
+
|
14
|
+
def get_diff(repo: Repo = None, against: str = "HEAD") -> PatchSet | list[PatchedFile]:
|
15
|
+
repo = repo or Repo(".")
|
16
|
+
base = repo.remotes.origin.refs.HEAD.reference.name
|
17
|
+
logging.info(f"{base}...{against}")
|
18
|
+
diff_content = repo.git.diff(base, against)
|
19
|
+
diff = PatchSet.from_string(diff_content)
|
20
|
+
return diff
|
21
|
+
|
22
|
+
|
23
|
+
def filter_diff(
|
24
|
+
patch_set: PatchSet | Iterable[PatchedFile], filters: str | list[str]
|
25
|
+
) -> PatchSet | Iterable[PatchedFile]:
|
26
|
+
"""
|
27
|
+
Filter the diff files by the given fnmatch filters.
|
28
|
+
"""
|
29
|
+
print([f.path for f in patch_set])
|
30
|
+
assert isinstance(filters, (list, str))
|
31
|
+
if not isinstance(filters, list):
|
32
|
+
filters = [f.strip() for f in filters.split(",") if f.strip()]
|
33
|
+
if not filters:
|
34
|
+
return patch_set
|
35
|
+
files = [
|
36
|
+
file
|
37
|
+
for file in patch_set
|
38
|
+
if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
|
39
|
+
]
|
40
|
+
print([f.path for f in files])
|
41
|
+
return files
|
42
|
+
|
43
|
+
|
44
|
+
def file_lines(repo: Repo, file: str, max_tokens: int = None) -> str:
|
45
|
+
text = repo.tree()[file].data_stream.read().decode()
|
46
|
+
lines = [f"{i + 1}: {line}\n" for i, line in enumerate(text.splitlines())]
|
47
|
+
if max_tokens:
|
48
|
+
lines, removed_qty = mc.tokenizing.fit_to_token_size(lines, max_tokens)
|
49
|
+
if removed_qty:
|
50
|
+
lines.append(
|
51
|
+
f"(!) DISPLAYING ONLY FIRST {len(lines)} LINES DUE TO LARGE FILE SIZE\n"
|
52
|
+
)
|
53
|
+
return "".join(lines)
|
54
|
+
|
55
|
+
|
56
|
+
def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
|
57
|
+
return mc.prompt(
|
58
|
+
cfg.summary_prompt,
|
59
|
+
diff=mc.tokenizing.fit_to_token_size(diff, cfg.max_code_tokens)[0],
|
60
|
+
issues=report.issues,
|
61
|
+
**cfg.prompt_vars,
|
62
|
+
).to_llm() if cfg.summary_prompt else ""
|
63
|
+
|
64
|
+
|
65
|
+
async def review(filters: str | list[str] = ""):
|
66
|
+
cfg = ProjectConfig.load()
|
67
|
+
repo = Repo(".")
|
68
|
+
diff = get_diff(repo=repo, against="HEAD")
|
69
|
+
diff = filter_diff(diff, filters)
|
70
|
+
if not diff:
|
71
|
+
logging.error("Nothing to review")
|
72
|
+
return
|
73
|
+
lines = {
|
74
|
+
file_diff.path: (
|
75
|
+
file_lines(
|
76
|
+
repo,
|
77
|
+
file_diff.path,
|
78
|
+
cfg.max_code_tokens
|
79
|
+
- mc.tokenizing.num_tokens_from_string(str(file_diff)),
|
80
|
+
)
|
81
|
+
if file_diff.target_file != DEV_NULL
|
82
|
+
else ""
|
83
|
+
)
|
84
|
+
for file_diff in diff
|
85
|
+
}
|
86
|
+
responses = await mc.llm_parallel(
|
87
|
+
[
|
88
|
+
mc.prompt(
|
89
|
+
cfg.prompt,
|
90
|
+
input=file_diff,
|
91
|
+
file_lines=lines[file_diff.path],
|
92
|
+
**cfg.prompt_vars,
|
93
|
+
)
|
94
|
+
for file_diff in diff
|
95
|
+
],
|
96
|
+
retries=cfg.retries,
|
97
|
+
parse_json=True,
|
98
|
+
)
|
99
|
+
issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
|
100
|
+
for file, file_issues in issues.items():
|
101
|
+
for issue in file_issues:
|
102
|
+
for i in issue.get("affected_lines", []):
|
103
|
+
if lines[file]:
|
104
|
+
f_lines = [""] + lines[file].splitlines()
|
105
|
+
i["affected_code"] = "\n".join(
|
106
|
+
f_lines[i["start_line"]: i["end_line"]+1]
|
107
|
+
)
|
108
|
+
exec(cfg.post_process, {"mc": mc, **locals()})
|
109
|
+
report = Report(issues=issues, number_of_processed_files=len(diff))
|
110
|
+
report.summary = make_cr_summary(cfg, report, diff)
|
111
|
+
report.save()
|
112
|
+
report_text = report.render(cfg, Report.Format.MARKDOWN)
|
113
|
+
print(mc.ui.yellow(report_text))
|
114
|
+
open("code-review-report.txt", "w", encoding="utf-8").write(report_text)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import logging
|
2
|
+
import tomllib
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import microcore as mc
|
7
|
+
|
8
|
+
from .constants import PROJECT_CONFIG_FILE, PROJECT_CONFIG_DEFAULTS_FILE
|
9
|
+
|
10
|
+
|
11
|
+
def _detect_github_env() -> dict:
|
12
|
+
"""
|
13
|
+
Try to detect GitHub repository/PR info from environment variables (for GitHub Actions).
|
14
|
+
Returns a dict with github_repo, github_pr_sha, github_pr_number, github_ref, etc.
|
15
|
+
"""
|
16
|
+
import os
|
17
|
+
|
18
|
+
repo = os.environ.get("GITHUB_REPOSITORY", "")
|
19
|
+
pr_sha = os.environ.get("GITHUB_SHA", "")
|
20
|
+
pr_number = os.environ.get("GITHUB_REF", "")
|
21
|
+
branch = ""
|
22
|
+
ref = os.environ.get("GITHUB_REF", "")
|
23
|
+
# Try to resolve PR head SHA if available.
|
24
|
+
# On PRs, GITHUB_HEAD_REF/BASE_REF contain branch names.
|
25
|
+
if "GITHUB_HEAD_REF" in os.environ:
|
26
|
+
branch = os.environ["GITHUB_HEAD_REF"]
|
27
|
+
elif ref.startswith("refs/heads/"):
|
28
|
+
branch = ref[len("refs/heads/"):]
|
29
|
+
elif ref.startswith("refs/pull/"):
|
30
|
+
# for pull_request events
|
31
|
+
branch = ref
|
32
|
+
|
33
|
+
d = {
|
34
|
+
"github_repo": repo,
|
35
|
+
"github_pr_sha": pr_sha,
|
36
|
+
"github_pr_number": pr_number,
|
37
|
+
"github_branch": branch,
|
38
|
+
"github_ref": ref,
|
39
|
+
}
|
40
|
+
# Fallback for local usage: try to get from git
|
41
|
+
if not repo:
|
42
|
+
try:
|
43
|
+
from git import Repo as GitRepo
|
44
|
+
|
45
|
+
git = GitRepo(".", search_parent_directories=True)
|
46
|
+
origin = git.remotes.origin.url
|
47
|
+
# e.g. git@github.com:Nayjest/ai-code-review.git -> Nayjest/ai-code-review
|
48
|
+
import re
|
49
|
+
|
50
|
+
match = re.search(r"[:/]([\w\-]+)/([\w\-\.]+?)(\.git)?$", origin)
|
51
|
+
if match:
|
52
|
+
d["github_repo"] = f"{match.group(1)}/{match.group(2)}"
|
53
|
+
d["github_pr_sha"] = git.head.commit.hexsha
|
54
|
+
d["github_branch"] = (
|
55
|
+
git.active_branch.name if hasattr(git, "active_branch") else ""
|
56
|
+
)
|
57
|
+
except Exception:
|
58
|
+
pass
|
59
|
+
# If branch is not a commit SHA, prefer branch for links
|
60
|
+
if d["github_branch"]:
|
61
|
+
d["github_pr_sha_or_branch"] = d["github_branch"]
|
62
|
+
elif d["github_pr_sha"]:
|
63
|
+
d["github_pr_sha_or_branch"] = d["github_pr_sha"]
|
64
|
+
else:
|
65
|
+
d["github_pr_sha_or_branch"] = "main"
|
66
|
+
return d
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class ProjectConfig:
|
71
|
+
prompt: str = ""
|
72
|
+
summary_prompt: str = ""
|
73
|
+
report_template_md: str = ""
|
74
|
+
"""Markdown report template"""
|
75
|
+
post_process: str = ""
|
76
|
+
retries: int = 3
|
77
|
+
"""LLM retries for one request"""
|
78
|
+
max_code_tokens: int = 32000
|
79
|
+
prompt_vars: dict = field(default_factory=dict)
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def load(custom_config_file: str | Path | None = None) -> "ProjectConfig":
|
83
|
+
config_file = Path(custom_config_file or PROJECT_CONFIG_FILE)
|
84
|
+
with open(PROJECT_CONFIG_DEFAULTS_FILE, "rb") as f:
|
85
|
+
config = tomllib.load(f)
|
86
|
+
github_env = _detect_github_env()
|
87
|
+
config["prompt_vars"] |= github_env | dict(github_env=github_env)
|
88
|
+
if config_file.exists():
|
89
|
+
logging.info(
|
90
|
+
f"Loading project-specific configuration from {mc.utils.file_link(config_file)}...")
|
91
|
+
default_prompt_vars = config["prompt_vars"]
|
92
|
+
with open(config_file, "rb") as f:
|
93
|
+
config.update(tomllib.load(f))
|
94
|
+
# overriding prompt_vars config section will not empty default values
|
95
|
+
config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
|
96
|
+
else:
|
97
|
+
logging.info(f"Config file {config_file} not found, using defaults")
|
98
|
+
|
99
|
+
return ProjectConfig(**config)
|