From a5514f2929a0a2c33e1c5b03984118c4b3bbf538 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sat, 7 Mar 2026 16:56:48 -0500 Subject: [PATCH 01/18] add activation events --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d25742a..502a795 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "categories": [ "Other" ], + "activationEvents": [], "main": "./out/extension.js", "contributes": { "commands": [ From 2afb7588a295bedfa264d29330a39713d6f53516 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Wed, 11 Mar 2026 19:55:10 -0400 Subject: [PATCH 02/18] first line selection --- CHANGELOG.md | 4 ++++ src/providers.ts | 4 ++++ src/test/suite/providers.test.ts | 41 ++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfff59..5e7f214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- When first line is focused, without a selection, show the whole file on GitHub (#74) + ## 3.4.0 - 2026-03-07 ### Changed diff --git a/src/providers.ts b/src/providers.ts index 8321b21..4bfeaa5 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -113,6 +113,10 @@ export class Github extends BaseProvider { PROVIDER_NAME = "github" buildLines({ start, end }: ISelection): string { + if (start.line === end.line && start.character === end.character) { + return "" + } + let line = `L${start.line + 1}` if (start.character !== 0) { line += `C${start.character + 1}` diff --git a/src/test/suite/providers.test.ts b/src/test/suite/providers.test.ts index 8531687..f2a3c71 100644 --- a/src/test/suite/providers.test.ts +++ b/src/test/suite/providers.test.ts @@ -150,6 +150,47 @@ suite("Github", async () => { assert.deepEqual(result, expected) } }) + test("if we have no selection on the first line, don't select anything", async () => { + for (let url of [ + "git@github.mycompany.com:recipeyak/recipeyak.git", + "git@github.mycompany.com:recipeyak/recipeyak", + "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", + "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", + ]) { + async function findRemote(hostname: string) { + return url + } + const gh = new Github( + { + github: { hostnames: ["github.mycompany.com"] }, + }, + "origin", + findRemote, + ) + const result = await gh.getUrls({ + selection: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + head: createBranch("master"), + relativeFilePath: "frontend/src/components/App.tsx", + }) + const expected = { + blobUrl: + "https://github.mycompany.com/recipeyak/recipeyak/blob/master/frontend/src/components/App.tsx", + blameUrl: + "https://github.mycompany.com/recipeyak/recipeyak/blame/master/frontend/src/components/App.tsx", + compareUrl: + "https://github.mycompany.com/recipeyak/recipeyak/compare/master", + historyUrl: + "https://github.mycompany.com/recipeyak/recipeyak/commits/master/frontend/src/components/App.tsx", + prUrl: + "https://github.mycompany.com/recipeyak/recipeyak/pull/new/master", + repoUrl: "https://github.mycompany.com/recipeyak/recipeyak", + } + assert.deepEqual(result, expected) + } + }) }) suite("Gitlab", async () => { From a3f859b980537a89ec8425312ca1505032bd1d73 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Thu, 12 Mar 2026 09:31:01 -0400 Subject: [PATCH 03/18] wip --- .vscode-test.mjs | 5 +- images/logo.sketch | Bin 13496 -> 14495 bytes package.json | 3 + src/extension.ts | 66 ++-- src/git.ts | 174 ++++------- src/test/suite/git.test.ts | 39 +-- src/test/suite/providers.test.ts | 8 +- src/utils.ts | 5 +- src/vscode-git.d.ts | 511 +++++++++++++++++++++++++++++++ 9 files changed, 633 insertions(+), 178 deletions(-) create mode 100644 src/vscode-git.d.ts diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 1ab7a73..6acfc3b 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,4 +1,7 @@ // .vscode-test.mjs import { defineConfig } from "@vscode/test-cli" -export default defineConfig({ files: "out/test/**/*.test.js" }) +export default defineConfig({ + files: "out/test/**/*.test.js", + workspaceFolder: ".", +}) diff --git a/images/logo.sketch b/images/logo.sketch index 1049889ee4852e170d16ca44a10cd6c784abfe29..19f7befd1d58a888016308e857bf0417c60f7d99 100644 GIT binary patch literal 14495 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ïv}O}fS?6`jPFKik*E zq(y>H+_f+CE89-8Y3@<$uzsz#{)vjnS&p6?1*+}wCE?M_9dyFLchRs?S*z35Qq(!% zENHHubQJ)Z%fdvVR)Lpg=FI1?Q)aKREpK#Bx!#> z<>f9^40$An%gsQ&dg}dMcq5Cu9X=PwbE=9zK1!WxfPLoCJe-5Mq5zDX92>YZH9PXC zpPdHkTV-s07b)=?Pw1H6u=LMR(t&qmhERF+XuU0cI8+pMC((x@AX*d4h6LmEu7tKI!et?7#)0_vF5bHUAm-(RLL z4%@OnCIT`pZD!p)H?rBaB?B?UU#O(1eJaCII!1NW?Idpy4^Oy3v@%AEksMbf8^_ksHr<3>fu5}B2e*t$ zkW)ABmWe5(eDv~LY;@bHH#&###Jl6*d%mnap2c)Ah!9h#a)mHQfc$3dy`3>L!Al>g zn`!k|@e1>&y8GQA+m}Xqh>HRpmw7z%6ZINo&4~9A{9H9&X4?-<7IedUb>t8%ryhH; z9I=91@wWBCla8X}Lget%-;@iv){OEG<3{1Yv(NW+mlq~xES@9co`K{xYN*a1m)$PD zsT>F1JrfsDVf?Nn{yj!~KVtL9baN}y`gGJ?+~U0i>2BP$lFLp|jR}DkUSPE)1q4eB zj3(CDPY+RFu)?slQB#j{BE`1v=+GzFGsyZqXw?RP*8)~zMzbRE80XZgYSbIZg?@kK z+D?>MwpmZ1+AOqmo}fM45G5qk+rHa5O0JLPPkMOKiQZ-WgjC1e>85b7g2+(#N!9d3 z7~D(G+-t7TFt1Is6)zSV`{A)gZEUXddVDE3wi=OR9t(43?B079(>%!QuJB!)KN%sK zklx{IPax9cm|Ou>sqFSM%%qjjT79k6&+2R3oGA>8qO61J<=I9k^=EV2x3w_3@^t*t zF<3S?oS)iQQ1|ByR#dNbzu9afDM3bkotN%%X&A=Dp z_v#hC+;nJ<_r;2y>)$ZlX$&zP#JiqVUZoS?Y6P&Hy!@G%g>b@hRrO zp82{b$<$5GJleND`EuIk$U6k0uiD7r(~8z!&MsW;+S34%Ox05_+qu&e)ljO!%>8V3JrQ+oW zX+v^iwYX)D8)Na=av?sd8QRqt5LnQP?G2d^T_f>2l9_+pagSW%iDCAT6$L49mmgjS38I234o1pxiJBfv5U*P)Ks1A z$)7zI+oFk`+l3ed>!csQj+#LLUEmn@XBBr0pjBsM-TnjWzQj8TvAC{7bFPowo7Qpk zkfqLefKsD}gAB)TETDOv#kuC%szZRv^INSPf35iik(*)`_cOrapoO<90bVM`U2--z zhTJHegXQqvzY(|cfvYMyY(QlBT+XGJu3d0RF7&v4XN@~Ho0pS3{CS(y?Pf-bN+7hL zM(B+~=rKujBm=e{)a3Dx!D%Q0h-iS}Q(?+?c^KsKy&VQHgnM;Dj$-8@RM(8(Bb0gX z)ulEiN|N3d^G=|_84mz?!E$BTWdqvS%`rA=rmN>@h8*11EUgV9sB1xSPkRViPy>-g zd?y%~rhCP5@~c2hAKl>*CFh{chLr-aBS4X1Y_UMt=NP=PTc7>enJ~xX(%RV~gpH^K z1)ZHx+A7Z=noC!kUz$KjbxXF@9y;)(MU2zU*ZKgV}!RwlpdMI{;(+c9_~2(<;ZD{Qcc(QG5-~Z z`SW)9Vg&+W|C8AWnb6iOwL-gc;hI4Yrs0pQM+ZhlXuL+`1%OYE&`<&3Fh0z$O}q8a zci<3#-_}u!Uo-5Rm>W|QZ<%v|-+%}rKq>VzQ~+l@)OzqdDs!o;`Y(RZf3*04!2-7EeW`=d#au8p#F2qd zx8b=@WGs`uPR7no=tEcC^5un|F6q7=bQbXb=-EgV;2Ab`Lv=YmK!Y>5ZS6PdSkBEy zP@c?yMy1hc7gn3SO;%HyR6za}1cYM`#~qH|-p(yTa2JyQrdNgi)?%LHxcaqlk`_<{ ztLI~4b+C~Ezwcib8Eap&&<9_dY|k0zS>S(@4|6z~&5hKGad&6wXL^)~L>^^IK0--V zY8&3kHA+aRRhCr{tqIn6m!gyNxS*?cT1}O-?7w6=tp$gIaR7;$?duP{C-@hgZ&Zr1 z+GJ5)z`ni`r(M~l(92Nra5-tqK#CT{VpLYAPiTsw@^HLpI>lLS(y|1h$bV$W;>? z6A9=|psz|a!iGk~?udIi$zKvjtllwb3X`o;8}R@cl9xJXvq9XVbgY%ew$MPfy2CnPMgTf(7%h;M|9=sifJ%#Ts~{pEX`fuXY{^7EB%GxO;y zZeV2VQR>d-wa=|Ha9x91C~HjpClPORobB}L}Qj%C1o zFLB35W{wT|yearbnzpQBPTgyF5u&8(bCG%X&1&nLk|{GFY@-r3#Q~_woTBffTJG0Iawv$0W#t{- z=eFIIFZ@QvO}Q!?88VQoMnbb}T0YDdzFjk13C~@G;S_F;<$YDq<2(kk`MsuU8~9Gh zc|t&iX6vt;9QV2k1J;@61e{pGW5E_i>`#{!x~$f|Z&!~}e2NAfI9317a!r;%QH2cL z^lV*MUl{4p9-kRHU^eoN&py83APjyt(fr3Sb+Ybd8F~-FJ8&LV@9)ld^lFLKgOc!g z{~r88Z{)i(!b~32W*^WD%B^nTMjE!;AsZ*bet}}e)>VxfVoDVc{ z+I zem|wV$MkOI-s@-MMeINc;bo2V-}KF5XfRqsaJ#E?B3qx^90WwxBYX%PTw7i06NJ~jec){N6{T(A6u>9G?47p`3dQN7i<{%h4+1cDN0(9R@# zq)O{%C#Qi+CnOiF?~s5dgabu}hfn`$61)6s{8nlGd39U`ZWa~Pge)7*GZtI4CXn8+ zLo7%T|GAop)1gp6*IG$x?Z~tHtP}WY>as zk{ugyrn4z6f{2Zr&aAoP24?o| z#2`Q_ICHv;yY4IWul}`NYbS2pdTZPfL-to;x3||(fDuA3G|vnH^kZ~!3fVLa`ZK0I z17&%6VTx`c>RYds%IJ2?rl##kP zh~Vnyp{l$QA>+#&SLTW0hn5mFjHwU`{I61Qxi6ptOVK2bb&%AcDb({Dr&;Cu`Td%v zbnJ*2+TjKdE|daqEgUAxvUHUXSMyQy&NBVO84g39$(k}5g^&{I&~Hu#g&(y-&Vy-F zg!beq2CrM=2cb~8&1cQUm;1;e-ja>t0hH_Hp`Q7sYYy;jb~}2!7pZ;gcZA80AGdJm z)US`A{?6wMARZk|+k&Gsxo(eEGxC`FqU_V@c|IPWTachRJYX>9F-#0|?Vo6ZKck~= z)@+9ue5*yqcF#$9pxE|j{3zXxL%cJ(BGQ0HZ|QBpN7PTuhS1u?KvM)th}*R!H+}CN zevMcrdOm1<+Ysov0SmwKC@2xd?C|mNS%Ng(w~}$UKG5)zZHHq`!zy5a-06Fn0oJKB zwWt*~ir=l@dh;-=n8Me&h9C|?q5rXlJ*6f1Sn~345XE3nV*l8sr6hO^Q>8G5uX`1L zn21F=f~G--c(1+3QOIrC0ZqY$IzalR)qNQ|XOrl<2gUQsHxUgyudlLY=xQata{uLt z=1kA%L1k8aQ7awACzPaUn2ZW4j@!0qZkBZbx1W-^=%sfxs7m1vN%1$*xe3I#E`}KZ zMhlZ=cyE_rWOnZqMpZjej&G=>Yp!Je-#rf;V#ngN)B>3*WwRo0;%aBV*X%X;)O-@;sB4E zN20p?HCvxx^GVGv4@0TRA>JfXihM|U6zR|#8C|2^7IiuTgy5xoO)VwBq>(@=`5o&=GTI) zd|s{{Z_1&~`7hWFaT!##9CLEz=HSJN@&3F;mfF}+RekkIb+=hj znLI{zJyiG9d)>g-t8?RVUDv&*BTI=vCmr9>VGgyP=4hN0V%)klOH z|48GxO!Ycxd!^6lIHf~C*ZjMM0y?9;AkXi&?a9vzu5$1LG)N50E(A_gxRFZe(Ji$= z&Qv3bjA^4>X=P^gZ`mRH7`QxvN(%!>J--g-K(xh|yr#K6>Nxu%zkZ=<_;WnGGSTU2 zI$NMz*>VS|0;T|@!HkZeT-G0?#2`jnHEQ=;4@R6nZ;Tll?$`Jy)-!=?LjOE+5EQS= z9@|KYExYq(6+MaPijK-J@t&RKSD42eiO}7=_cs#Z-z=g4^RIyi9y7egs{SvS3h#3s z0p>ueb&n6e`%-Rsq?`kukoY!8=m4uGh$cs3uDZuc8kSh)vdh@h=ZhsiIrOz6(Wc6E zIbv?{Au{aDXOB0^BI)yfCGwAzKZV}E~n+A7#C43^7O)GSI(AoYEsQoc85G<(2-I%aM z4LxGoDVqJIofS%a%u>5-pvWXf@18D9BltBo080=h8 zfY!ecEwF5%@?-idYf6K4mpI5A4ZvrKVdV685V?8re74bE7Ln(Uw$a$v zNp9zL0R(18Lg;2m@L**BevP6L_QLpvq$4ZJ+0vO{W;C`2RdiEaascnMgsjdlD6sb6 zs*o&CGD|mr_ua0EEb@=Y1?s3NM?S5?Sq+K=|)i_9APS#+M+JRs;)( z&seXNOIeQGgG1XS$kZd{y0#aioTthiQRP%Txb%41!(Ef|I(0Futm zq@(GU{+BdexzciRC749Q=jLXc2D{4--Whw7We7Ba9<<9M?>|s>3A`%xM!cV?{xBBG z)Wq%FUDbSD=zF+zQgq(TZtI&5U|_G07iD~)0zkJZv6)2@ajplQ6A!opXC7&?wA>W} zvjPt#qNhs*I&bVZ&e!)}p17w)QOYN4@KI~;mF?JGJQQa5p9y3ZuW!- z6n^t`?sb<~azZCOTKY=)SgXTT3NfJ<_xsE128i!uBKBPxCSV)nrMHR4bvWUHn{w59 zXFOSIwW7qM-xvI_$A$1$Sh36z2)MiHaoW944Ha)`K>X?@{NUr%?hokI5EL3t8Sosr zrJvvPl3A)PSg__O1|xqO2rYV_E&`ZTTYBjp&OT>w+UI`|&h^{l(?1v>TyN(xNPhW) zNvJiFTLvHrZ&|lTv-!cVeyT+dC;DF0L=o6;o|7A#^4*GQ!;EzqZ~U2f<-C+XZTqSp)c zjivc`&*?M~E6jR>D+KMIrxw$WZAp9$3Xt`{e!dfw@jf4~crA0hm)EQl2C&5(QL^0( zbGPyPawyo={W7(0U@)GGI`6R>wx+B*qJQp5u@62jHhzmO!tC0Pr@uQpoR?gCPz}VG z`xDU1TLb&@8Sa_#ao=X)CMD^9ndDsXmV4b$(#V4lT|Q}vz+)f9#%BC%DgN2e*md}w zwRR8DcQosY-D%koq^4effW`DN5%m`3bU(g!FD*GY`Ax0SA9u|pg?CR?yLl{=+v&(G zJbm_^!tm&|Zm23s&IiTn)+bZvZPam7IiN(vB#=9>r*k z6{v%j)~bY!{am}3xc=ntJMz@m-WlhEm<5a1bV9N70eZ6c}|GBa>36l(loUz?_g{Wmeb=qrr> z3z54e&zlw!lIcK0xI|ps)fCOB^UoTOx&tOc_9V~khPM&tPjU105y_P&mw>)~NPyc+ zdqWP;l)YieOGh9ZgzIGoV>fpV`l*L?DO?DkEA zHxAAFNW51FQ4;CE8X8CK2Dt{E4=e|Gp3gTHbJb7~6W^u?VN&#$bIiu;hEyhBCqzUK zNaqySD9@gkMpjF(-o(0V2T(ax>g0eYDKSs?&d9j`^ccJ2`9P%3%^LGW6=@5e1ybXS z=e6~PF3jk3rd*NZO59AwtOwJiKrm7+zdWgeyC7?6k_^4Mv4YL;mE#n zO$&rN1Ld@LoL=N3YP$B68kZfb3+=B@?Vx8dNU&kwhBoZ;LC~8#{YZ_ZM;;`YBqm{3 zgh4b)PtxDsS(okz&1BB?N;YB8#W|X#ZZ2?zqs3TUhz$>jZoTujq_{CR!hUwjW_fG= z>HALWB+>zc6TWCTMEy*=U#Bg@9g;&=9NmBG8VMBJSKaadURMZ5Qs&6>@4H|sEsdye6LC?=CDX}Hpo)R4! zwyD^2PlztvFi_y5a1H&i@hrKvOo2DeLIjT@22GL#3#b2!2=LyO#8&{*+`(^NM@|&Q zD3qhm^Tg;`q zcxVMaai1e@N9l{4o_O)1GMT z>*SW-u(iMJ*0p(dU|6q)>SZsDhj8)P*#gvDQ*#QsnfI?}v;M2ee7%|Z7hQTxI9iovQiufXe zXV1%xIJN0ba|<8=(w}-|^v}2Hx9sZcdr%AW*u5*!iujQaEpJ*7ov!IP_OB>ZB_DEZ zbqENhyqS$4dL5W@Ug9jj@I_jPcn`rt3GJhM5nGDNdK%D8C?Y}oVEYL4p2s2)Dg1HN zGjJM_dDAz-oaV1RT{c)Bn*}=Rst_x-46Yi9+#+C}NDJOHz{^bAf=C2F&jj)j2mF(p z+dorpYYwA2ExA?|fLu~vfvvT5Ec|A3s?wc^@_f;LG(|QiXzlHVS*R{+s1rN?s!Wdl zpDo&-O`E+}*j*RPcy3qPp_C?LIY`|Gw(W5|H23LF1)JW8N4|uwZDtv#8ln)X7yyaO z-=m-t0#3f;K871%Geef?)H?nGSstsv>1Xuc@?Nidqz7peUsV1K5b1bMZ#cy2lML-Q zjqSP)MnegqIT_(j>OS!t37esI${K5DN&o!;ch@V6N}0MtSw_R(5x!8u15-Uj##&AIy&YQdhZ z@Ljg%3q}@Q+(J@Um}|wB{XB^^?`C!?Ixosn&Ahw|4itCSyZ_OpkCf^dR#)I3P13LA za}_nm`c|xfLa1u4Y6$2lay&6cv)o;gP*tK$p*&KRVQ)bqXI%a>mijlA9`NMfWWv zLxHCWk0SY5@jrkB+y}6{E)pFYTa1${Plg$9@C&LUbt`MMd}ui-zgyPq%!fVk;Lvy3 z7LYVK2rHC0f$n|Zti_-qoYU$33=4(^ZSFr_opRle*u9{5{aPrDj@NGZP3#(UK4|a| z%TGe`aG#`IKUS5(UEcjxHNl&?1KisC&&D-vKIM;E&9F~*$z?KH&&%a%8uW)rG+))H zEEPW%`ciz9Cwmi;-1)$?-K(g->(iF1L9nj?@s56py{qTZwQ}MX2v^lQIuD|^dc%Ym zrHRuZYIUUxR0ey}k0B!NXbPc`4Ulb3=>3J3%)3Xf?fw6ea_?7$%X&4T&u{P-bt5gV zAXX{zIp8mv=t|s({+{x0DggZRUr83?4f*pg6S{x5y!mTF_n+=R4C?-O(SL`({WZ<` zm)m{=2>pLF)cN0)|2qopFM{r0F8vMU^gnpI|6T0g0aX84%pM&8{s*x3&tm^HBmCz~ m{hcMOe>L?MI>vY#%Kw42Qj&#+`I`~??I-nygc9cZ8~cCQdj~K8 literal 13496 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�=yfqL}#49w0vz``GJ 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 gui8Hw*Ngu { const disposable = vscode.commands.registerCommand(cmd, () => githubinator(args), ) context.subscriptions.push(disposable) }) - vscode.commands.registerCommand( + const openFromUrlDisposable = vscode.commands.registerCommand( "githubinator.githubinatorOpenFromUrl", openFileFromGitHubUrl, ) + context.subscriptions.push(openFromUrlDisposable) console.log("githubinator.active.complete") } @@ -127,11 +133,15 @@ function mainBranches() { .get("mainBranches", ["main"]) } +/** + * Search default main branch names for the first one that exists. + */ async function findShaForBranches( - gitDir: string, + gitRepository: Repository, + fileUri: vscode.Uri, ): Promise<[string, string] | null> { for (let branch of mainBranches()) { - const sha = await git.getSHAForBranch(gitDir, branch) + const sha = await git.getSHAForBranch(gitRepository, branch) if (sha == null) { continue } @@ -151,41 +161,41 @@ interface IGithubinator { openPR?: boolean compare?: boolean } -async function githubinator({ - openUrl, - copyToClipboard, - blame, - mainBranch, - openRepo, - permalink, - history, - openPR, - compare, -}: IGithubinator) { - console.log("githubinator.call") +async function githubinator(options: IGithubinator) { + const { + openUrl, + copyToClipboard, + blame, + mainBranch, + openRepo, + permalink, + history, + openPR, + compare, + } = options + outputChannel.appendLine( + "githubinator called with options: " + JSON.stringify(options), + ) const editorConfig = getEditorInfo() if (!editorConfig.uri) { - return err("could not find file") + return err("Could not find file for current editor.") } + const fileUri = editorConfig.uri - const gitDirectories = git.dir(editorConfig.uri.fsPath) - - if (gitDirectories == null) { - return err("Could not find .git directory.") + const gitRepository = await git.getRepo(fileUri) + if (!gitRepository) { + return err("Could not find git repository for file.") } - const gitDir = gitDirectories.git - const repoDir = gitDirectories.repository - let headBranch: [string, string | null] | null = null if (mainBranch) { - const res = await findShaForBranches(gitDir) + const res = await findShaForBranches(gitRepository, fileUri) if (res == null) { return err(`Could not find SHA for branch in ${mainBranches()}`) } headBranch = res } else { - headBranch = await git.head(gitDir) + headBranch = await git.head(gitRepository) } if (headBranch == null) { return err("Could not find HEAD.") @@ -209,7 +219,7 @@ async function githubinator({ const parsedUrl = await new provider( providersConfig, globalDefaultRemote, - (remote) => git.origin(gitDir, remote), + (remote) => git.origin(gitRepository, remote), ).getUrls({ selection, // priority: permalink > branch > branch from HEAD @@ -220,7 +230,7 @@ async function githubinator({ ? createSha(head) : createBranch(branchName), relativeFilePath: editorConfig.fileName - ? getRelativeFilePath(repoDir, editorConfig.fileName) + ? getRelativeFilePath(gitRepository.rootUri, editorConfig.fileName) : null, }) if (parsedUrl != null) { @@ -232,7 +242,7 @@ async function githubinator({ } if (urls == null) { - return err("Could not find provider for repo.") + return err("Could not find remote for repository.") } let url = compare diff --git a/src/git.ts b/src/git.ts index 4df9866..690b6b0 100644 --- a/src/git.ts +++ b/src/git.ts @@ -1,139 +1,77 @@ -import * as path from "path" -import * as fs from "mz/fs" -import * as ini from "ini" +import * as vscode from "vscode" +import { outputChannel } from "./extension" +import { GitExtension, Remote, Repository } from "./vscode-git" -interface IRemote { - fetch: string - url?: string +async function getGitAPI() { + const gitExtension = + vscode.extensions.getExtension("vscode.git") + if (!gitExtension) return null + const exports = await gitExtension.activate() + return exports.getAPI(1) } -interface IGitDirectories { - git: string - repository: string +export async function getRepo(fileUri: vscode.Uri) { + const api = await getGitAPI() + if (!api) { + vscode.window.showErrorMessage("Git API not available") + return null + } + const existing = api.getRepository(fileUri) + if (existing) return existing + outputChannel.appendLine("No repository found, triggering git refresh...") + await vscode.commands.executeCommand("git.refresh") + const afterRefresh = api.getRepository(fileUri) + if (afterRefresh) return afterRefresh + // Repos are discovered asynchronously after activation; wait for one to open + return new Promise>((resolve) => { + const timeout = setTimeout(() => { + disposable.dispose() + resolve(null) + outputChannel.appendLine("Hit 5 second timeout for repository to load.") + }, 5000) + const disposable = api.onDidOpenRepository(() => { + const repo = api.getRepository(fileUri) + if (repo) { + outputChannel.appendLine("Found repository via onDidOpenRepository.") + clearTimeout(timeout) + disposable.dispose() + resolve(repo) + } + }) + }) } export async function origin( - gitDir: string, + repository: Repository, remote: string, ): Promise { - const configPath = path.resolve(gitDir, "config") - if (!(await fs.exists(configPath))) { - return null - } - const configFileData = await fs.readFile(configPath, { encoding: "utf-8" }) - const parsedConfig = ini.parse(configFileData) - for (const [key, value] of Object.entries(parsedConfig)) { - if (key.startsWith('remote "')) { - const origin = key.replace(/^remote "/, "").replace(/"$/, "") - if (origin === remote) { - const url = (value as IRemote).url - return url || null - } - } - } - - return null + const found = repository.state.remotes.find((r: Remote) => r.name === remote) + return found?.fetchUrl ?? found?.pushUrl ?? null } -/** Get the SHA for a ref */ export async function getSHAForBranch( - gitDir: string, + repository: Repository, branchName: string, ): Promise { - const refName = `refs/heads/${branchName}` - // check for normal ref - const refPath = path.resolve(gitDir, refName) - if (await fs.exists(refPath)) { - return await fs.readFile(refPath, { - encoding: "utf-8", - }) - } - // check packed-refs - const packedRefPath = path.resolve(gitDir, "packed-refs") - if (await fs.exists(packedRefPath)) { - const packRefs = await fs.readFile(packedRefPath, { - encoding: "utf-8", - }) - - for (const x of packRefs.split("\n")) { - const [sha, refPath] = x.split(" ") as [ - string | undefined, - string | undefined, - ] - if (sha && refPath && refPath.trim() === refName.trim()) { - return sha - } - } + try { + const branch = await repository.getBranch(branchName) + return branch.commit ?? null + } catch { + return null } - return null } -/** Get the current SHA and branch from HEAD for a git directory */ export async function head( - gitDir: string, + repository: Repository, ): Promise<[string, string | null] | null> { - const headPath = path.resolve(gitDir, "HEAD") - if (!(await fs.exists(headPath))) { - return null - } - const headFileData = await fs.readFile(headPath, { encoding: "utf-8" }) - if (!headFileData) { - return null - } - // If we're not on a branch, headFileData will be of the form: - // `3c0cc80bbdb682f6e9f65b4c9659ca21924aad4` - // If we're on a branch, it will be `ref: refs/heads/my_branch_name` - const [maybeSha, maybeHeadInfo] = headFileData.split(" ") as [ - string, - string | undefined, - ] - if (maybeHeadInfo == null) { - return [maybeSha.trim(), null] - } - const branchName = maybeHeadInfo.trim().replace("refs/heads/", "") - const sha = await getSHAForBranch(gitDir, branchName) - if (sha == null) { - return null - } - return [sha.trim(), branchName] -} - -export function dir(filePath: string) { - return walkUpDirectories(filePath, ".git") + const repoHead = repository.state.HEAD + if (!repoHead?.commit) return null + return [repoHead.commit, repoHead.name ?? null] } -function walkUpDirectories( - file_path: string, - file_or_folder: string, -): IGitDirectories | null { - let directory = file_path - while (true) { - const newPath = path.resolve(directory, file_or_folder) - if (fs.existsSync(newPath)) { - if (fs.lstatSync(newPath).isFile()) { - const submoduleMatch = fs - .readFileSync(newPath, "utf8") - .match(/gitdir: (.+)/) - - if (submoduleMatch) { - return { - git: path.resolve(path.join(directory, submoduleMatch[1])), - repository: directory, - } - } else { - return null - } - } else { - return { - git: newPath, - repository: directory, - } - } - } - const newDirectory = path.dirname(directory) - if (newDirectory === directory) { - return null - } - directory = newDirectory - } +export async function dir( + repository: Repository, +): Promise<{ git: string; repository: string } | null> { + const root = repository.rootUri.fsPath + return { git: root, repository: root } } diff --git a/src/test/suite/git.test.ts b/src/test/suite/git.test.ts index 398d750..f82361d 100644 --- a/src/test/suite/git.test.ts +++ b/src/test/suite/git.test.ts @@ -1,30 +1,19 @@ import { dir } from "../../git" import * as assert from "assert" import * as path from "path" -import * as fs from "fs" -suite("git", async () => { - test("dir", () => { - const repoPath = path.normalize(path.join(__dirname, "../../..")) - const gitPath = path.join(repoPath, ".git") +// suite("git", async () => { +// test("dir", async () => { +// const repoPath = path.normalize(path.join(__dirname, "../../..")) - assert.deepStrictEqual(dir(__dirname), { - git: gitPath, - repository: repoPath, - }) - assert.deepStrictEqual(dir(repoPath), { - git: gitPath, - repository: repoPath, - }) - - const contents = "gitdir: ../../../../.git/modules/test_submodule" - const submodulePath = path.join(__dirname, "test_submodule") - fs.mkdirSync(submodulePath, { recursive: true }) - fs.writeFileSync(path.join(submodulePath, ".git"), contents) - - assert.deepStrictEqual(dir(submodulePath), { - git: path.join(repoPath, ".git/modules/test_submodule"), - repository: submodulePath, - }) - }) -}) +// // dir() activates vscode.git before querying, so no manual wait needed +// assert.deepStrictEqual(await dir(__dirname), { +// git: repoPath, +// repository: repoPath, +// }) +// assert.deepStrictEqual(await dir(repoPath), { +// git: repoPath, +// repository: repoPath, +// }) +// }) +// }) diff --git a/src/test/suite/providers.test.ts b/src/test/suite/providers.test.ts index f2a3c71..a2b710a 100644 --- a/src/test/suite/providers.test.ts +++ b/src/test/suite/providers.test.ts @@ -40,7 +40,7 @@ suite("Github", async () => { "git@github.com:recipeyak/recipeyak", "org-XYZ123@github.com:recipeyak/recipeyak", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github({}, "origin", findRemote) @@ -75,7 +75,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github( @@ -116,7 +116,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github( @@ -157,7 +157,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github( diff --git a/src/utils.ts b/src/utils.ts index ec7a2c5..c29cd52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,14 @@ import * as path from "path" import * as fs from "fs" +import * as vscode from "vscode" /** Get path of file relative to repository root. */ export function getRelativeFilePath( - repositoryDir: string, + repositoryDir: vscode.Uri, fileName: string, ): string | null { try { const resolvedFileName = fs.realpathSync(fileName) - return resolvedFileName.replace(repositoryDir, "") + return resolvedFileName.replace(repositoryDir.fsPath, "") } catch (e) { if ( typeof e === "object" && diff --git a/src/vscode-git.d.ts b/src/vscode-git.d.ts new file mode 100644 index 0000000..27b4915 --- /dev/null +++ b/src/vscode-git.d.ts @@ -0,0 +1,511 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; +export { ProviderResult } from 'vscode'; + +export interface Git { + readonly path: string; +} + +export interface InputBox { + value: string; +} + +export const enum ForcePushMode { + Force, + ForceWithLease, + ForceWithLeaseIfIncludes, +} + +export const enum RefType { + Head, + RemoteHead, + Tag +} + +export interface Ref { + readonly type: RefType; + readonly name?: string; + readonly commit?: string; + readonly commitDetails?: Commit; + readonly remote?: string; +} + +export interface UpstreamRef { + readonly remote: string; + readonly name: string; + readonly commit?: string; +} + +export interface Branch extends Ref { + readonly upstream?: UpstreamRef; + readonly ahead?: number; + readonly behind?: number; +} + +export interface CommitShortStat { + readonly files: number; + readonly insertions: number; + readonly deletions: number; +} + +export interface Commit { + readonly hash: string; + readonly message: string; + readonly parents: string[]; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; + readonly commitDate?: Date; + readonly shortStat?: CommitShortStat; +} + +export interface Submodule { + readonly name: string; + readonly path: string; + readonly url: string; +} + +export interface Remote { + readonly name: string; + readonly fetchUrl?: string; + readonly pushUrl?: string; + readonly isReadOnly: boolean; +} + +export interface Worktree { + readonly name: string; + readonly path: string; + readonly ref: string; + readonly main: boolean; + readonly detached: boolean; +} + +export const enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + INTENT_TO_ADD, + INTENT_TO_RENAME, + TYPE_CHANGED, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED +} + +export interface Change { + + /** + * Returns either `originalUri` or `renameUri`, depending + * on whether this change is a rename change. When + * in doubt always use `uri` over the other two alternatives. + */ + readonly uri: Uri; + readonly originalUri: Uri; + readonly renameUri: Uri | undefined; + readonly status: Status; +} + +export interface DiffChange extends Change { + readonly insertions: number; + readonly deletions: number; +} + +export type RepositoryKind = 'repository' | 'submodule' | 'worktree'; + +export interface RepositoryState { + readonly HEAD: Branch | undefined; + readonly refs: Ref[]; + readonly remotes: Remote[]; + readonly submodules: Submodule[]; + readonly worktrees: Worktree[]; + readonly rebaseCommit: Commit | undefined; + + readonly mergeChanges: Change[]; + readonly indexChanges: Change[]; + readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; + + readonly onDidChange: Event; +} + +export interface RepositoryUIState { + readonly selected: boolean; + readonly onDidChange: Event; +} + +export interface RepositoryAccessDetails { + readonly rootUri: Uri; + readonly lastAccessTime: number; +} + +/** + * Log options. + */ +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; + readonly path?: string; + /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ + readonly range?: string; + readonly reverse?: boolean; + readonly sortByAuthorDate?: boolean; + readonly shortStats?: boolean; + readonly author?: string; + readonly grep?: string; + readonly refNames?: string[]; + readonly maxParents?: number; + readonly skip?: number; +} + +export interface CommitOptions { + all?: boolean | 'tracked'; + amend?: boolean; + signoff?: boolean; + /** + * true - sign the commit + * false - do not sign the commit + * undefined - use the repository/global git config + */ + signCommit?: boolean; + empty?: boolean; + noVerify?: boolean; + requireUserConfig?: boolean; + useEditor?: boolean; + verbose?: boolean; + /** + * string - execute the specified command after the commit operation + * undefined - execute the command specified in git.postCommitCommand + * after the commit operation + * null - do not execute any command after the commit operation + */ + postCommitCommand?: string | null; +} + +export interface FetchOptions { + remote?: string; + ref?: string; + all?: boolean; + prune?: boolean; + depth?: number; +} + +export interface InitOptions { + defaultBranch?: string; +} + +export interface CloneOptions { + parentPath?: Uri; + /** + * ref is only used if the repository cache is missed. + */ + ref?: string; + recursive?: boolean; + /** + * If no postCloneAction is provided, then the users setting for git.openAfterClone is used. + */ + postCloneAction?: 'none'; +} + +export interface RefQuery { + readonly contains?: string; + readonly count?: number; + readonly pattern?: string | string[]; + readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; +} + +export interface BranchQuery extends RefQuery { + readonly remote?: boolean; +} + +export interface Repository { + + readonly rootUri: Uri; + readonly inputBox: InputBox; + readonly state: RepositoryState; + readonly ui: RepositoryUIState; + readonly kind: RepositoryKind; + + readonly onDidCommit: Event; + readonly onDidCheckout: Event; + + getConfigs(): Promise<{ key: string; value: string; }[]>; + getConfig(key: string): Promise; + setConfig(key: string, value: string): Promise; + unsetConfig(key: string): Promise; + getGlobalConfig(key: string): Promise; + + getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; + detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>; + buffer(ref: string, path: string): Promise; + show(ref: string, path: string): Promise; + getCommit(ref: string): Promise; + + add(paths: string[]): Promise; + revert(paths: string[]): Promise; + clean(paths: string[]): Promise; + + apply(patch: string, reverse?: boolean): Promise; + apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean; }): Promise; + diff(cached?: boolean): Promise; + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWithHEADShortStats(path?: string): Promise; + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWithHEADShortStats(path?: string): Promise; + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffBlobs(object1: string, object2: string): Promise; + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise; + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; + + hashObject(data: string): Promise; + + createBranch(name: string, checkout: boolean, ref?: string): Promise; + deleteBranch(name: string, force?: boolean): Promise; + getBranch(name: string): Promise; + getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise; + getBranchBase(name: string): Promise; + setBranchUpstream(name: string, upstream: string): Promise; + + checkIgnore(paths: string[]): Promise>; + + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; + + getMergeBase(ref1: string, ref2: string): Promise; + + tag(name: string, message: string, ref?: string | undefined): Promise; + deleteTag(name: string): Promise; + + status(): Promise; + checkout(treeish: string): Promise; + + addRemote(name: string, url: string): Promise; + removeRemote(name: string): Promise; + renameRemote(name: string, newName: string): Promise; + + fetch(options?: FetchOptions): Promise; + fetch(remote?: string, ref?: string, depth?: number): Promise; + pull(unshallow?: boolean): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise; + + blame(path: string): Promise; + log(options?: LogOptions): Promise; + + commit(message: string, opts?: CommitOptions): Promise; + merge(ref: string): Promise; + mergeAbort(): Promise; + rebase(branch: string): Promise; + + createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise; + applyStash(index?: number): Promise; + popStash(index?: number): Promise; + dropStash(index?: number): Promise; + + createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree(path: string, options?: { force?: boolean }): Promise; + + migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; + + generateRandomBranchName(): Promise; + + isBranchProtected(branch?: Branch): boolean; +} + +export interface RemoteSource { + readonly name: string; + readonly description?: string; + readonly url: string | string[]; +} + +export interface RemoteSourceProvider { + readonly name: string; + readonly icon?: string; // codicon name + readonly supportsQuery?: boolean; + getRemoteSources(query?: string): ProviderResult; + getBranches?(url: string): ProviderResult; + publishRepository?(repository: Repository): Promise; +} + +export interface RemoteSourcePublisher { + readonly name: string; + readonly icon?: string; // codicon name + publishRepository(repository: Repository): Promise; +} + +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + +export interface PostCommitCommandsProvider { + getCommands(repository: Repository): Command[]; +} + +export interface PushErrorHandler { + handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise; +} + +export interface BranchProtection { + readonly remote: string; + readonly rules: BranchProtectionRule[]; +} + +export interface BranchProtectionRule { + readonly include?: string[]; + readonly exclude?: string[]; +} + +export interface BranchProtectionProvider { + onDidChangeBranchProtection: Event; + provideBranchProtection(): BranchProtection[]; +} + +export interface AvatarQueryCommit { + readonly hash: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + +export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult>; + provideHoverCommands(repository: Repository): ProviderResult; + provideMessageLinks(repository: Repository, message: string): ProviderResult; +} + +export type APIState = 'uninitialized' | 'initialized'; + +export interface PublishEvent { + repository: Repository; + branch?: string; +} + +export interface API { + readonly state: APIState; + readonly onDidChangeState: Event; + readonly onDidPublish: Event; + readonly git: Git; + readonly repositories: Repository[]; + readonly recentRepositories: Iterable; + readonly onDidOpenRepository: Event; + readonly onDidCloseRepository: Event; + + toGitUri(uri: Uri, ref: string): Uri; + getRepository(uri: Uri): Repository | null; + getRepositoryRoot(uri: Uri): Promise; + getRepositoryWorkspace(uri: Uri): Promise; + init(root: Uri, options?: InitOptions): Promise; + /** + * Checks the cache of known cloned repositories, and clones if the repository is not found. + * Make sure to pass `postCloneAction` 'none' if you want to have the uri where you can find the repository returned. + * @returns The URI of a folder or workspace file which, when opened, will open the cloned repository. + */ + clone(uri: Uri, options?: CloneOptions): Promise; + openRepository(root: Uri): Promise; + + registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; + registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; + registerPushErrorHandler(handler: PushErrorHandler): Disposable; + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; +} + +export interface GitExtension { + + readonly enabled: boolean; + readonly onDidChangeEnablement: Event; + + /** + * Returns a specific API version. + * + * Throws error if git extension is disabled. You can listen to the + * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event + * to know when the extension becomes enabled/disabled. + * + * @param version Version number. + * @returns API instance + */ + getAPI(version: 1): API; +} + +export const enum GitErrorCodes { + BadConfigFile = 'BadConfigFile', + BadRevision = 'BadRevision', + AuthenticationFailed = 'AuthenticationFailed', + NoUserNameConfigured = 'NoUserNameConfigured', + NoUserEmailConfigured = 'NoUserEmailConfigured', + NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified', + NotAGitRepository = 'NotAGitRepository', + NotASafeGitRepository = 'NotASafeGitRepository', + NotAtRepositoryRoot = 'NotAtRepositoryRoot', + Conflict = 'Conflict', + StashConflict = 'StashConflict', + UnmergedChanges = 'UnmergedChanges', + PushRejected = 'PushRejected', + ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected', + ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected', + RemoteConnectionError = 'RemoteConnectionError', + DirtyWorkTree = 'DirtyWorkTree', + CantOpenResource = 'CantOpenResource', + GitNotFound = 'GitNotFound', + CantCreatePipe = 'CantCreatePipe', + PermissionDenied = 'PermissionDenied', + CantAccessRemote = 'CantAccessRemote', + RepositoryNotFound = 'RepositoryNotFound', + RepositoryIsLocked = 'RepositoryIsLocked', + BranchNotFullyMerged = 'BranchNotFullyMerged', + NoRemoteReference = 'NoRemoteReference', + InvalidBranchName = 'InvalidBranchName', + BranchAlreadyExists = 'BranchAlreadyExists', + NoLocalChanges = 'NoLocalChanges', + NoStashFound = 'NoStashFound', + LocalChangesOverwritten = 'LocalChangesOverwritten', + NoUpstreamBranch = 'NoUpstreamBranch', + IsInSubmodule = 'IsInSubmodule', + WrongCase = 'WrongCase', + CantLockRef = 'CantLockRef', + CantRebaseMultipleBranches = 'CantRebaseMultipleBranches', + PatchDoesNotApply = 'PatchDoesNotApply', + NoPathFound = 'NoPathFound', + UnknownPath = 'UnknownPath', + EmptyCommitMessage = 'EmptyCommitMessage', + BranchFastForwardRejected = 'BranchFastForwardRejected', + BranchNotYetBorn = 'BranchNotYetBorn', + TagConflict = 'TagConflict', + CherryPickEmpty = 'CherryPickEmpty', + CherryPickConflict = 'CherryPickConflict', + WorktreeContainsChanges = 'WorktreeContainsChanges', + WorktreeAlreadyExists = 'WorktreeAlreadyExists', + WorktreeBranchAlreadyUsed = 'WorktreeBranchAlreadyUsed' +} From 7b9306c3cd000a89b6a0be5b69d5166a623f46b8 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Thu, 12 Mar 2026 09:36:08 -0400 Subject: [PATCH 04/18] use git binary --- src/extension.ts | 3 +- src/git.ts | 99 ++++++++++++++++++++++++++---------------------- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 435c1d4..0b84e53 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,6 @@ import * as git from "./git" import { providers, IUrlInfo, createSha, createBranch } from "./providers" import { getRelativeFilePath } from "./utils" import { openFileFromGitHubUrl } from "./openfromUrl" -import { Repository } from "./vscode-git" const COMMANDS: [string, IGithubinator][] = [ [ @@ -137,7 +136,7 @@ function mainBranches() { * Search default main branch names for the first one that exists. */ async function findShaForBranches( - gitRepository: Repository, + gitRepository: git.Repo, fileUri: vscode.Uri, ): Promise<[string, string] | null> { for (let branch of mainBranches()) { diff --git a/src/git.ts b/src/git.ts index 690b6b0..cc16cb5 100644 --- a/src/git.ts +++ b/src/git.ts @@ -1,77 +1,84 @@ import * as vscode from "vscode" +import { execFile } from "child_process" +import { promisify } from "util" +import * as path from "path" import { outputChannel } from "./extension" -import { GitExtension, Remote, Repository } from "./vscode-git" -async function getGitAPI() { - const gitExtension = - vscode.extensions.getExtension("vscode.git") - if (!gitExtension) return null - const exports = await gitExtension.activate() - return exports.getAPI(1) +const execFileAsync = promisify(execFile) + +export interface Repo { + rootUri: vscode.Uri +} + +async function git(cwd: string, ...args: string[]): Promise { + const { stdout } = await execFileAsync("git", args, { cwd }) + return stdout.trim() } -export async function getRepo(fileUri: vscode.Uri) { - const api = await getGitAPI() - if (!api) { - vscode.window.showErrorMessage("Git API not available") +export async function getRepo(fileUri: vscode.Uri): Promise { + try { + const stat = await vscode.workspace.fs.stat(fileUri) + const cwd = + stat.type & vscode.FileType.Directory + ? fileUri.fsPath + : path.dirname(fileUri.fsPath) + const root = await git(cwd, "rev-parse", "--show-toplevel") + return { rootUri: vscode.Uri.file(root) } + } catch { + outputChannel.appendLine( + "Could not find git repository for file: " + fileUri.fsPath, + ) return null } - const existing = api.getRepository(fileUri) - if (existing) return existing - outputChannel.appendLine("No repository found, triggering git refresh...") - await vscode.commands.executeCommand("git.refresh") - const afterRefresh = api.getRepository(fileUri) - if (afterRefresh) return afterRefresh - // Repos are discovered asynchronously after activation; wait for one to open - return new Promise>((resolve) => { - const timeout = setTimeout(() => { - disposable.dispose() - resolve(null) - outputChannel.appendLine("Hit 5 second timeout for repository to load.") - }, 5000) - const disposable = api.onDidOpenRepository(() => { - const repo = api.getRepository(fileUri) - if (repo) { - outputChannel.appendLine("Found repository via onDidOpenRepository.") - clearTimeout(timeout) - disposable.dispose() - resolve(repo) - } - }) - }) } export async function origin( - repository: Repository, + repo: Repo, remote: string, ): Promise { - const found = repository.state.remotes.find((r: Remote) => r.name === remote) - return found?.fetchUrl ?? found?.pushUrl ?? null + try { + return await git(repo.rootUri.fsPath, "remote", "get-url", remote) + } catch { + return null + } } export async function getSHAForBranch( - repository: Repository, + repo: Repo, branchName: string, ): Promise { try { - const branch = await repository.getBranch(branchName) - return branch.commit ?? null + return await git(repo.rootUri.fsPath, "rev-parse", branchName) } catch { return null } } export async function head( - repository: Repository, + repo: Repo, ): Promise<[string, string | null] | null> { - const repoHead = repository.state.HEAD - if (!repoHead?.commit) return null - return [repoHead.commit, repoHead.name ?? null] + try { + const sha = await git(repo.rootUri.fsPath, "rev-parse", "HEAD") + let branchName: string | null = null + try { + branchName = await git( + repo.rootUri.fsPath, + "symbolic-ref", + "--short", + "HEAD", + ) + } catch { + // detached HEAD, branchName stays null + } + return [sha, branchName] + } catch { + return null + } } export async function dir( - repository: Repository, + repo: Repo, ): Promise<{ git: string; repository: string } | null> { - const root = repository.rootUri.fsPath + const root = repo.rootUri.fsPath return { git: root, repository: root } } From f3ec13d8405ea77dd6df4aad798cfcc5a46360bb Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:11:47 -0400 Subject: [PATCH 05/18] delete old file --- src/test/suite/git.test.ts | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/test/suite/git.test.ts diff --git a/src/test/suite/git.test.ts b/src/test/suite/git.test.ts deleted file mode 100644 index f82361d..0000000 --- a/src/test/suite/git.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { dir } from "../../git" -import * as assert from "assert" -import * as path from "path" - -// suite("git", async () => { -// test("dir", async () => { -// const repoPath = path.normalize(path.join(__dirname, "../../..")) - -// // dir() activates vscode.git before querying, so no manual wait needed -// assert.deepStrictEqual(await dir(__dirname), { -// git: repoPath, -// repository: repoPath, -// }) -// assert.deepStrictEqual(await dir(repoPath), { -// git: repoPath, -// repository: repoPath, -// }) -// }) -// }) From 4a57f9dac61df704d587661f4031ada32f43bfc7 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:18:54 -0400 Subject: [PATCH 06/18] wip --- package.json | 2 +- src/extension.ts | 20 +++++++++++--------- src/git.ts | 2 +- yarn.lock | 8 ++++---- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 054143e..1e86ef3 100644 --- a/package.json +++ b/package.json @@ -456,7 +456,7 @@ "@types/mocha": "^10.0.6", "@types/mz": "^2.7.3", "@types/node": "^15.12.4", - "@types/vscode": "^1.32.0", + "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", "esbuild": "^0.11.12", diff --git a/src/extension.ts b/src/extension.ts index 0b84e53..45216e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -77,11 +77,13 @@ export interface IGithubinatorConfig { } } -export let outputChannel: vscode.OutputChannel +export let outputChannel: vscode.LogOutputChannel export function activate(context: vscode.ExtensionContext) { - console.log("githubinator.active.start") - outputChannel = vscode.window.createOutputChannel("GitHubinator") + outputChannel = vscode.window.createOutputChannel("GitHubinator", { + log: true, + }) + outputChannel.debug("githubinator.active.start") context.subscriptions.push(outputChannel) COMMANDS.forEach(([cmd, args]) => { const disposable = vscode.commands.registerCommand(cmd, () => @@ -95,15 +97,15 @@ export function activate(context: vscode.ExtensionContext) { ) context.subscriptions.push(openFromUrlDisposable) - console.log("githubinator.active.complete") + outputChannel.debug("githubinator.active.complete") } export function deactivate() { - console.log("githubinator.deactivate") + outputChannel.debug("githubinator.deactivate") } function err(message: string) { - console.error(message) + outputChannel.error(message) vscode.window.showErrorMessage(message) } @@ -172,7 +174,7 @@ async function githubinator(options: IGithubinator) { openPR, compare, } = options - outputChannel.appendLine( + outputChannel.info( "githubinator called with options: " + JSON.stringify(options), ) const editorConfig = getEditorInfo() @@ -233,11 +235,11 @@ async function githubinator(options: IGithubinator) { : null, }) if (parsedUrl != null) { - console.log("Found provider", provider.name) + outputChannel.debug("Found provider", provider.name) urls = parsedUrl break } - console.log("Skipping provider", provider.name) + outputChannel.debug("Skipping provider", provider.name) } if (urls == null) { diff --git a/src/git.ts b/src/git.ts index cc16cb5..23a3ba4 100644 --- a/src/git.ts +++ b/src/git.ts @@ -25,7 +25,7 @@ export async function getRepo(fileUri: vscode.Uri): Promise { const root = await git(cwd, "rev-parse", "--show-toplevel") return { rootUri: vscode.Uri.file(root) } } catch { - outputChannel.appendLine( + outputChannel.warn( "Could not find git repository for file: " + fileUri.fsPath, ) return null diff --git a/yarn.lock b/yarn.lock index 3148f77..06030b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,10 +89,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== -"@types/vscode@^1.32.0": - version "1.57.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.57.0.tgz#cc648e0573b92f725cd1baf2621f8da9f8bc689f" - integrity sha512-FeznBFtIDCWRluojTsi9c3LLcCHOXP5etQfBK42+ixo1CoEAchkw39tuui9zomjZuKfUVL33KZUDIwHZ/xvOkQ== +"@types/vscode@^1.110.0": + version "1.110.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.110.0.tgz#b6210c7d5e049003138bb17311644fe8b179dc8b" + integrity sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA== "@vscode/test-cli@^0.0.9": version "0.0.9" From 47a75d8f1c4360216ac2cebf0e6ef24964b1c7ff Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:27:16 -0400 Subject: [PATCH 07/18] wip --- package.json | 4 ++-- src/git.ts | 21 ++++++--------------- yarn.lock | 22 +++++++++++++++++----- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 1e86ef3..fcc0760 100644 --- a/package.json +++ b/package.json @@ -455,14 +455,14 @@ "@types/lodash": "^4.14.170", "@types/mocha": "^10.0.6", "@types/mz": "^2.7.3", - "@types/node": "^15.12.4", + "@types/node": "^22.22.0", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", "esbuild": "^0.11.12", "mocha": "^10.4.0", "prettier": "^3.3.0", - "typescript": "^4.2.2", + "typescript": "^5.9.3", "vscode-test": "^1.6.1" }, "dependencies": { diff --git a/src/git.ts b/src/git.ts index 23a3ba4..3369c12 100644 --- a/src/git.ts +++ b/src/git.ts @@ -1,15 +1,15 @@ import * as vscode from "vscode" -import { execFile } from "child_process" -import { promisify } from "util" -import * as path from "path" +import { execFile } from "node:child_process" +import { promisify } from "node:util" +import * as path from "node:path" import { outputChannel } from "./extension" -const execFileAsync = promisify(execFile) - export interface Repo { rootUri: vscode.Uri } +const execFileAsync = promisify(execFile) + async function git(cwd: string, ...args: string[]): Promise { const { stdout } = await execFileAsync("git", args, { cwd }) return stdout.trim() @@ -25,9 +25,7 @@ export async function getRepo(fileUri: vscode.Uri): Promise { const root = await git(cwd, "rev-parse", "--show-toplevel") return { rootUri: vscode.Uri.file(root) } } catch { - outputChannel.warn( - "Could not find git repository for file: " + fileUri.fsPath, - ) + outputChannel.warn("Could not find git repository for file", fileUri.fsPath) return null } } @@ -75,10 +73,3 @@ export async function head( return null } } - -export async function dir( - repo: Repo, -): Promise<{ git: string; repository: string } | null> { - const root = repo.rootUri.fsPath - return { git: root, repository: root } -} diff --git a/yarn.lock b/yarn.lock index 06030b0..196b7cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -84,11 +84,18 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^15.12.4": +"@types/node@*": version "15.12.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== +"@types/node@^22.22.0": + version "22.19.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.15.tgz#6091d99fdf7c08cb57dc8b1345d407ba9a1df576" + integrity sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg== + dependencies: + undici-types "~6.21.0" + "@types/vscode@^1.110.0": version "1.110.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.110.0.tgz#b6210c7d5e049003138bb17311644fe8b179dc8b" @@ -1433,10 +1440,15 @@ to-regex-range@^5.0.1: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== -typescript@^4.2.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" - integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== unzipper@^0.10.11: version "0.10.14" From d86732d1617ef087d06329199d706a6d305105e7 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:28:04 -0400 Subject: [PATCH 08/18] fix --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fcc0760..e827efb 100644 --- a/package.json +++ b/package.json @@ -455,7 +455,7 @@ "@types/lodash": "^4.14.170", "@types/mocha": "^10.0.6", "@types/mz": "^2.7.3", - "@types/node": "^22.22.0", + "@types/node": "^22.19.15", "@types/vscode": "^1.110.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 196b7cf..48ecb00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -89,7 +89,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== -"@types/node@^22.22.0": +"@types/node@^22.19.15": version "22.19.15" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.15.tgz#6091d99fdf7c08cb57dc8b1345d407ba9a1df576" integrity sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg== From db70883a344eb5e7f2487c1f24ff4072a3002c4c Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:40:20 -0400 Subject: [PATCH 09/18] . --- src/vscode-git.d.ts | 511 -------------------------------------------- 1 file changed, 511 deletions(-) delete mode 100644 src/vscode-git.d.ts diff --git a/src/vscode-git.d.ts b/src/vscode-git.d.ts deleted file mode 100644 index 27b4915..0000000 --- a/src/vscode-git.d.ts +++ /dev/null @@ -1,511 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; -export { ProviderResult } from 'vscode'; - -export interface Git { - readonly path: string; -} - -export interface InputBox { - value: string; -} - -export const enum ForcePushMode { - Force, - ForceWithLease, - ForceWithLeaseIfIncludes, -} - -export const enum RefType { - Head, - RemoteHead, - Tag -} - -export interface Ref { - readonly type: RefType; - readonly name?: string; - readonly commit?: string; - readonly commitDetails?: Commit; - readonly remote?: string; -} - -export interface UpstreamRef { - readonly remote: string; - readonly name: string; - readonly commit?: string; -} - -export interface Branch extends Ref { - readonly upstream?: UpstreamRef; - readonly ahead?: number; - readonly behind?: number; -} - -export interface CommitShortStat { - readonly files: number; - readonly insertions: number; - readonly deletions: number; -} - -export interface Commit { - readonly hash: string; - readonly message: string; - readonly parents: string[]; - readonly authorDate?: Date; - readonly authorName?: string; - readonly authorEmail?: string; - readonly commitDate?: Date; - readonly shortStat?: CommitShortStat; -} - -export interface Submodule { - readonly name: string; - readonly path: string; - readonly url: string; -} - -export interface Remote { - readonly name: string; - readonly fetchUrl?: string; - readonly pushUrl?: string; - readonly isReadOnly: boolean; -} - -export interface Worktree { - readonly name: string; - readonly path: string; - readonly ref: string; - readonly main: boolean; - readonly detached: boolean; -} - -export const enum Status { - INDEX_MODIFIED, - INDEX_ADDED, - INDEX_DELETED, - INDEX_RENAMED, - INDEX_COPIED, - - MODIFIED, - DELETED, - UNTRACKED, - IGNORED, - INTENT_TO_ADD, - INTENT_TO_RENAME, - TYPE_CHANGED, - - ADDED_BY_US, - ADDED_BY_THEM, - DELETED_BY_US, - DELETED_BY_THEM, - BOTH_ADDED, - BOTH_DELETED, - BOTH_MODIFIED -} - -export interface Change { - - /** - * Returns either `originalUri` or `renameUri`, depending - * on whether this change is a rename change. When - * in doubt always use `uri` over the other two alternatives. - */ - readonly uri: Uri; - readonly originalUri: Uri; - readonly renameUri: Uri | undefined; - readonly status: Status; -} - -export interface DiffChange extends Change { - readonly insertions: number; - readonly deletions: number; -} - -export type RepositoryKind = 'repository' | 'submodule' | 'worktree'; - -export interface RepositoryState { - readonly HEAD: Branch | undefined; - readonly refs: Ref[]; - readonly remotes: Remote[]; - readonly submodules: Submodule[]; - readonly worktrees: Worktree[]; - readonly rebaseCommit: Commit | undefined; - - readonly mergeChanges: Change[]; - readonly indexChanges: Change[]; - readonly workingTreeChanges: Change[]; - readonly untrackedChanges: Change[]; - - readonly onDidChange: Event; -} - -export interface RepositoryUIState { - readonly selected: boolean; - readonly onDidChange: Event; -} - -export interface RepositoryAccessDetails { - readonly rootUri: Uri; - readonly lastAccessTime: number; -} - -/** - * Log options. - */ -export interface LogOptions { - /** Max number of log entries to retrieve. If not specified, the default is 32. */ - readonly maxEntries?: number; - readonly path?: string; - /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ - readonly range?: string; - readonly reverse?: boolean; - readonly sortByAuthorDate?: boolean; - readonly shortStats?: boolean; - readonly author?: string; - readonly grep?: string; - readonly refNames?: string[]; - readonly maxParents?: number; - readonly skip?: number; -} - -export interface CommitOptions { - all?: boolean | 'tracked'; - amend?: boolean; - signoff?: boolean; - /** - * true - sign the commit - * false - do not sign the commit - * undefined - use the repository/global git config - */ - signCommit?: boolean; - empty?: boolean; - noVerify?: boolean; - requireUserConfig?: boolean; - useEditor?: boolean; - verbose?: boolean; - /** - * string - execute the specified command after the commit operation - * undefined - execute the command specified in git.postCommitCommand - * after the commit operation - * null - do not execute any command after the commit operation - */ - postCommitCommand?: string | null; -} - -export interface FetchOptions { - remote?: string; - ref?: string; - all?: boolean; - prune?: boolean; - depth?: number; -} - -export interface InitOptions { - defaultBranch?: string; -} - -export interface CloneOptions { - parentPath?: Uri; - /** - * ref is only used if the repository cache is missed. - */ - ref?: string; - recursive?: boolean; - /** - * If no postCloneAction is provided, then the users setting for git.openAfterClone is used. - */ - postCloneAction?: 'none'; -} - -export interface RefQuery { - readonly contains?: string; - readonly count?: number; - readonly pattern?: string | string[]; - readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; -} - -export interface BranchQuery extends RefQuery { - readonly remote?: boolean; -} - -export interface Repository { - - readonly rootUri: Uri; - readonly inputBox: InputBox; - readonly state: RepositoryState; - readonly ui: RepositoryUIState; - readonly kind: RepositoryKind; - - readonly onDidCommit: Event; - readonly onDidCheckout: Event; - - getConfigs(): Promise<{ key: string; value: string; }[]>; - getConfig(key: string): Promise; - setConfig(key: string, value: string): Promise; - unsetConfig(key: string): Promise; - getGlobalConfig(key: string): Promise; - - getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; - detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>; - buffer(ref: string, path: string): Promise; - show(ref: string, path: string): Promise; - getCommit(ref: string): Promise; - - add(paths: string[]): Promise; - revert(paths: string[]): Promise; - clean(paths: string[]): Promise; - - apply(patch: string, reverse?: boolean): Promise; - apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean; }): Promise; - diff(cached?: boolean): Promise; - diffWithHEAD(): Promise; - diffWithHEAD(path: string): Promise; - diffWithHEADShortStats(path?: string): Promise; - diffWith(ref: string): Promise; - diffWith(ref: string, path: string): Promise; - diffIndexWithHEAD(): Promise; - diffIndexWithHEAD(path: string): Promise; - diffIndexWithHEADShortStats(path?: string): Promise; - diffIndexWith(ref: string): Promise; - diffIndexWith(ref: string, path: string): Promise; - diffBlobs(object1: string, object2: string): Promise; - diffBetween(ref1: string, ref2: string): Promise; - diffBetween(ref1: string, ref2: string, path: string): Promise; - diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise; - diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; - - hashObject(data: string): Promise; - - createBranch(name: string, checkout: boolean, ref?: string): Promise; - deleteBranch(name: string, force?: boolean): Promise; - getBranch(name: string): Promise; - getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise; - getBranchBase(name: string): Promise; - setBranchUpstream(name: string, upstream: string): Promise; - - checkIgnore(paths: string[]): Promise>; - - getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; - - getMergeBase(ref1: string, ref2: string): Promise; - - tag(name: string, message: string, ref?: string | undefined): Promise; - deleteTag(name: string): Promise; - - status(): Promise; - checkout(treeish: string): Promise; - - addRemote(name: string, url: string): Promise; - removeRemote(name: string): Promise; - renameRemote(name: string, newName: string): Promise; - - fetch(options?: FetchOptions): Promise; - fetch(remote?: string, ref?: string, depth?: number): Promise; - pull(unshallow?: boolean): Promise; - push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise; - - blame(path: string): Promise; - log(options?: LogOptions): Promise; - - commit(message: string, opts?: CommitOptions): Promise; - merge(ref: string): Promise; - mergeAbort(): Promise; - rebase(branch: string): Promise; - - createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise; - applyStash(index?: number): Promise; - popStash(index?: number): Promise; - dropStash(index?: number): Promise; - - createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise; - deleteWorktree(path: string, options?: { force?: boolean }): Promise; - - migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; - - generateRandomBranchName(): Promise; - - isBranchProtected(branch?: Branch): boolean; -} - -export interface RemoteSource { - readonly name: string; - readonly description?: string; - readonly url: string | string[]; -} - -export interface RemoteSourceProvider { - readonly name: string; - readonly icon?: string; // codicon name - readonly supportsQuery?: boolean; - getRemoteSources(query?: string): ProviderResult; - getBranches?(url: string): ProviderResult; - publishRepository?(repository: Repository): Promise; -} - -export interface RemoteSourcePublisher { - readonly name: string; - readonly icon?: string; // codicon name - publishRepository(repository: Repository): Promise; -} - -export interface Credentials { - readonly username: string; - readonly password: string; -} - -export interface CredentialsProvider { - getCredentials(host: Uri): ProviderResult; -} - -export interface PostCommitCommandsProvider { - getCommands(repository: Repository): Command[]; -} - -export interface PushErrorHandler { - handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise; -} - -export interface BranchProtection { - readonly remote: string; - readonly rules: BranchProtectionRule[]; -} - -export interface BranchProtectionRule { - readonly include?: string[]; - readonly exclude?: string[]; -} - -export interface BranchProtectionProvider { - onDidChangeBranchProtection: Event; - provideBranchProtection(): BranchProtection[]; -} - -export interface AvatarQueryCommit { - readonly hash: string; - readonly authorName?: string; - readonly authorEmail?: string; -} - -export interface AvatarQuery { - readonly commits: AvatarQueryCommit[]; - readonly size: number; -} - -export interface SourceControlHistoryItemDetailsProvider { - provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult>; - provideHoverCommands(repository: Repository): ProviderResult; - provideMessageLinks(repository: Repository, message: string): ProviderResult; -} - -export type APIState = 'uninitialized' | 'initialized'; - -export interface PublishEvent { - repository: Repository; - branch?: string; -} - -export interface API { - readonly state: APIState; - readonly onDidChangeState: Event; - readonly onDidPublish: Event; - readonly git: Git; - readonly repositories: Repository[]; - readonly recentRepositories: Iterable; - readonly onDidOpenRepository: Event; - readonly onDidCloseRepository: Event; - - toGitUri(uri: Uri, ref: string): Uri; - getRepository(uri: Uri): Repository | null; - getRepositoryRoot(uri: Uri): Promise; - getRepositoryWorkspace(uri: Uri): Promise; - init(root: Uri, options?: InitOptions): Promise; - /** - * Checks the cache of known cloned repositories, and clones if the repository is not found. - * Make sure to pass `postCloneAction` 'none' if you want to have the uri where you can find the repository returned. - * @returns The URI of a folder or workspace file which, when opened, will open the cloned repository. - */ - clone(uri: Uri, options?: CloneOptions): Promise; - openRepository(root: Uri): Promise; - - registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; - registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; - registerCredentialsProvider(provider: CredentialsProvider): Disposable; - registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; - registerPushErrorHandler(handler: PushErrorHandler): Disposable; - registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; - registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; -} - -export interface GitExtension { - - readonly enabled: boolean; - readonly onDidChangeEnablement: Event; - - /** - * Returns a specific API version. - * - * Throws error if git extension is disabled. You can listen to the - * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event - * to know when the extension becomes enabled/disabled. - * - * @param version Version number. - * @returns API instance - */ - getAPI(version: 1): API; -} - -export const enum GitErrorCodes { - BadConfigFile = 'BadConfigFile', - BadRevision = 'BadRevision', - AuthenticationFailed = 'AuthenticationFailed', - NoUserNameConfigured = 'NoUserNameConfigured', - NoUserEmailConfigured = 'NoUserEmailConfigured', - NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified', - NotAGitRepository = 'NotAGitRepository', - NotASafeGitRepository = 'NotASafeGitRepository', - NotAtRepositoryRoot = 'NotAtRepositoryRoot', - Conflict = 'Conflict', - StashConflict = 'StashConflict', - UnmergedChanges = 'UnmergedChanges', - PushRejected = 'PushRejected', - ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected', - ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected', - RemoteConnectionError = 'RemoteConnectionError', - DirtyWorkTree = 'DirtyWorkTree', - CantOpenResource = 'CantOpenResource', - GitNotFound = 'GitNotFound', - CantCreatePipe = 'CantCreatePipe', - PermissionDenied = 'PermissionDenied', - CantAccessRemote = 'CantAccessRemote', - RepositoryNotFound = 'RepositoryNotFound', - RepositoryIsLocked = 'RepositoryIsLocked', - BranchNotFullyMerged = 'BranchNotFullyMerged', - NoRemoteReference = 'NoRemoteReference', - InvalidBranchName = 'InvalidBranchName', - BranchAlreadyExists = 'BranchAlreadyExists', - NoLocalChanges = 'NoLocalChanges', - NoStashFound = 'NoStashFound', - LocalChangesOverwritten = 'LocalChangesOverwritten', - NoUpstreamBranch = 'NoUpstreamBranch', - IsInSubmodule = 'IsInSubmodule', - WrongCase = 'WrongCase', - CantLockRef = 'CantLockRef', - CantRebaseMultipleBranches = 'CantRebaseMultipleBranches', - PatchDoesNotApply = 'PatchDoesNotApply', - NoPathFound = 'NoPathFound', - UnknownPath = 'UnknownPath', - EmptyCommitMessage = 'EmptyCommitMessage', - BranchFastForwardRejected = 'BranchFastForwardRejected', - BranchNotYetBorn = 'BranchNotYetBorn', - TagConflict = 'TagConflict', - CherryPickEmpty = 'CherryPickEmpty', - CherryPickConflict = 'CherryPickConflict', - WorktreeContainsChanges = 'WorktreeContainsChanges', - WorktreeAlreadyExists = 'WorktreeAlreadyExists', - WorktreeBranchAlreadyUsed = 'WorktreeBranchAlreadyUsed' -} From faaac6a7e2d4c00d3bcbd41531f2a77794afdd2a Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:40:50 -0400 Subject: [PATCH 10/18] bump --- .github/workflows/javascript.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 330cd3e..223e0eb 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests @@ -22,9 +22,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests From e10e694990eea792ba50da5d7a7f59faa464fae9 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:42:29 -0400 Subject: [PATCH 11/18] node-version-file --- .github/workflows/javascript.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 223e0eb..2f9ca94 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -14,6 +14,8 @@ jobs: uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests @@ -25,6 +27,8 @@ jobs: uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests From 2dce4728a5221931eebeb4fa8f96019916cecab0 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:44:21 -0400 Subject: [PATCH 12/18] wip --- .nvmrc | 1 + package.json | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..85e5027 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.22.0 diff --git a/package.json b/package.json index e827efb..2a79003 100644 --- a/package.json +++ b/package.json @@ -475,9 +475,5 @@ "extensionDependencies": [ "vscode.git" ], - "packageManager": "yarn@1.22.22", - "volta": { - "node": "18.20.3", - "yarn": "1.22.22" - } + "packageManager": "yarn@1.22.22" } From 7696e8297cc9e9cd4d118a83c75c8155806e1f93 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:45:36 -0400 Subject: [PATCH 13/18] format --- .github/workflows/javascript.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml index 2f9ca94..b8640a9 100644 --- a/.github/workflows/javascript.yml +++ b/.github/workflows/javascript.yml @@ -15,7 +15,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests @@ -28,7 +28,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Install dependencies run: yarn install --frozen-lockfile - name: Run tests From ae1475fe0708e324debd8170919fc7bda7cd6722 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:49:50 -0400 Subject: [PATCH 14/18] changelog --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7f214..02852cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## 4.0.0 - 2026-03-16 + +### Changed + +- Extension now calls `git` instead of manually reading `.git/` files. (#75) + ### Changed - When first line is focused, without a selection, show the whole file on GitHub (#74) diff --git a/package.json b/package.json index 2a79003..50827f8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "githubinator", "displayName": "Githubinator", "description": "Quickly open files on Github and other providers. View blame information, copy permalinks and more. See the \"commands\" section of the README for more details.", - "version": "3.4.0", + "version": "4.0.0", "publisher": "chdsbd", "license": "SEE LICENSE IN LICENSE", "icon": "images/logo256.png", From 7f5206f37f535e9873fa00c591a0b098b3996eda Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:52:08 -0400 Subject: [PATCH 15/18] don't change functionality --- src/providers.ts | 4 --- src/test/suite/providers.test.ts | 47 ++------------------------------ 2 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/providers.ts b/src/providers.ts index 4bfeaa5..8321b21 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -113,10 +113,6 @@ export class Github extends BaseProvider { PROVIDER_NAME = "github" buildLines({ start, end }: ISelection): string { - if (start.line === end.line && start.character === end.character) { - return "" - } - let line = `L${start.line + 1}` if (start.character !== 0) { line += `C${start.character + 1}` diff --git a/src/test/suite/providers.test.ts b/src/test/suite/providers.test.ts index a2b710a..8531687 100644 --- a/src/test/suite/providers.test.ts +++ b/src/test/suite/providers.test.ts @@ -40,7 +40,7 @@ suite("Github", async () => { "git@github.com:recipeyak/recipeyak", "org-XYZ123@github.com:recipeyak/recipeyak", ]) { - const findRemote = async (hostname: string) => { + async function findRemote(hostname: string) { return url } const gh = new Github({}, "origin", findRemote) @@ -75,7 +75,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - const findRemote = async (hostname: string) => { + async function findRemote(hostname: string) { return url } const gh = new Github( @@ -116,7 +116,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - const findRemote = async (hostname: string) => { + async function findRemote(hostname: string) { return url } const gh = new Github( @@ -150,47 +150,6 @@ suite("Github", async () => { assert.deepEqual(result, expected) } }) - test("if we have no selection on the first line, don't select anything", async () => { - for (let url of [ - "git@github.mycompany.com:recipeyak/recipeyak.git", - "git@github.mycompany.com:recipeyak/recipeyak", - "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", - "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", - ]) { - const findRemote = async (hostname: string) => { - return url - } - const gh = new Github( - { - github: { hostnames: ["github.mycompany.com"] }, - }, - "origin", - findRemote, - ) - const result = await gh.getUrls({ - selection: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - }, - head: createBranch("master"), - relativeFilePath: "frontend/src/components/App.tsx", - }) - const expected = { - blobUrl: - "https://github.mycompany.com/recipeyak/recipeyak/blob/master/frontend/src/components/App.tsx", - blameUrl: - "https://github.mycompany.com/recipeyak/recipeyak/blame/master/frontend/src/components/App.tsx", - compareUrl: - "https://github.mycompany.com/recipeyak/recipeyak/compare/master", - historyUrl: - "https://github.mycompany.com/recipeyak/recipeyak/commits/master/frontend/src/components/App.tsx", - prUrl: - "https://github.mycompany.com/recipeyak/recipeyak/pull/new/master", - repoUrl: "https://github.mycompany.com/recipeyak/recipeyak", - } - assert.deepEqual(result, expected) - } - }) }) suite("Gitlab", async () => { From 2a2ff882e5b136fbc54d903d3ef141cbb1b592f9 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:52:33 -0400 Subject: [PATCH 16/18] . --- .vscode-test.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 6acfc3b..1ab7a73 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,7 +1,4 @@ // .vscode-test.mjs import { defineConfig } from "@vscode/test-cli" -export default defineConfig({ - files: "out/test/**/*.test.js", - workspaceFolder: ".", -}) +export default defineConfig({ files: "out/test/**/*.test.js" }) From 01cdcec302861bb4deb67515a86b731db6632f1a Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:53:12 -0400 Subject: [PATCH 17/18] . --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02852cf..5d5600a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extension now calls `git` instead of manually reading `.git/` files. (#75) -### Changed - -- When first line is focused, without a selection, show the whole file on GitHub (#74) - ## 3.4.0 - 2026-03-07 ### Changed From b5b98e2816f3cf702bb66b8ea9cea9b1a957e58a Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Mon, 16 Mar 2026 21:54:23 -0400 Subject: [PATCH 18/18] fix --- src/test/suite/providers.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/suite/providers.test.ts b/src/test/suite/providers.test.ts index 8531687..998904b 100644 --- a/src/test/suite/providers.test.ts +++ b/src/test/suite/providers.test.ts @@ -40,7 +40,7 @@ suite("Github", async () => { "git@github.com:recipeyak/recipeyak", "org-XYZ123@github.com:recipeyak/recipeyak", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github({}, "origin", findRemote) @@ -75,7 +75,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github( @@ -116,7 +116,7 @@ suite("Github", async () => { "org-XYZ123@github.mycompany.com:recipeyak/recipeyak", "ssh://git@github.mycompany.com/recipeyak/recipeyak.git", ]) { - async function findRemote(hostname: string) { + const findRemote = async (hostname: string) => { return url } const gh = new Github(