@@ -7,7 +7,7 @@ import constants from '../constants.mts'
77import type { CResult } from '../types.mts'
88import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn'
99
10- export async function getBaseGitBranch ( cwd = process . cwd ( ) ) : Promise < string > {
10+ export async function getBaseBranch ( cwd = process . cwd ( ) ) : Promise < string > {
1111 // Lazily access constants.ENV properties.
1212 const { GITHUB_BASE_REF , GITHUB_REF_NAME , GITHUB_REF_TYPE } = constants . ENV
1313 // 1. In a pull request, this is always the base branch.
@@ -30,11 +30,67 @@ export async function getBaseGitBranch(cwd = process.cwd()): Promise<string> {
3030 return match [ 0 ] . trim ( )
3131 }
3232 } catch { }
33- // GitHub defaults to branch name "main"
33+ // GitHub and GitLab default to branch name "main"
3434 // https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches#about-the-default-branch
3535 return 'main'
3636}
3737
38+ export type RepoInfo = {
39+ owner : string
40+ repo : string
41+ }
42+
43+ export async function getRepoInfo (
44+ cwd = process . cwd ( ) ,
45+ ) : Promise < RepoInfo | null > {
46+ let info = null
47+ try {
48+ const remoteUrl = (
49+ await spawn ( 'git' , [ 'remote' , 'get-url' , 'origin' ] , { cwd } )
50+ ) . stdout
51+ info = parseGitRemoteUrl ( remoteUrl )
52+ if ( ! info ) {
53+ debugFn ( 'error' , 'git: unmatched git remote URL format' )
54+ debugDir ( 'inspect' , { remoteUrl } )
55+ }
56+ } catch ( e ) {
57+ debugFn ( 'error' , 'caught: `git remote get-url origin` failed' )
58+ debugDir ( 'inspect' , { error : e } )
59+ }
60+ return info
61+ }
62+
63+ export async function getRepoName ( cwd = process . cwd ( ) ) : Promise < string | null > {
64+ const repoInfo = await getRepoInfo ( cwd )
65+ return repoInfo ?. repo ?? null
66+ }
67+
68+ export async function getRepoOwner (
69+ cwd = process . cwd ( ) ,
70+ ) : Promise < string | null > {
71+ const repoInfo = await getRepoInfo ( cwd )
72+ return repoInfo ?. owner ?? null
73+ }
74+
75+ export async function gitBranch ( cwd = process . cwd ( ) ) : Promise < string | null > {
76+ const stdioPipeOptions : SpawnOptions = { cwd }
77+ // Try symbolic-ref first which returns the branch name or fails in a
78+ // detached HEAD state.
79+ try {
80+ return (
81+ await spawn ( 'git' , [ 'symbolic-ref' , '--short' , 'HEAD' ] , stdioPipeOptions )
82+ ) . stdout
83+ } catch { }
84+ // Fallback to using rev-parse to get the short commit hash in a
85+ // detached HEAD state.
86+ try {
87+ return (
88+ await spawn ( 'git' , [ 'rev-parse' , '--short' , 'HEAD' ] , stdioPipeOptions )
89+ ) . stdout
90+ } catch { }
91+ return null
92+ }
93+
3894export type GitCreateAndPushBranchOptions = {
3995 cwd ?: string | undefined
4096 email ?: string | undefined
@@ -227,42 +283,6 @@ export async function gitRemoteBranchExists(
227283 return false
228284}
229285
230- export type RepoInfo = {
231- owner : string
232- repo : string
233- }
234-
235- export async function gitRepoInfo (
236- cwd = process . cwd ( ) ,
237- ) : Promise < RepoInfo | null > {
238- try {
239- const remoteUrl = (
240- await spawn ( 'git' , [ 'remote' , 'get-url' , 'origin' ] , { cwd } )
241- ) . stdout
242- // 1. Handle SSH-style, e.g. git@github.com:owner/repo.git
243- const sshMatch = / ^ g i t @ [ ^ : ] + : ( [ ^ / ] + ) \/ ( .+ ?) (?: \. g i t ) ? $ / . exec ( remoteUrl )
244- if ( sshMatch ) {
245- return { owner : sshMatch [ 1 ] ! , repo : sshMatch [ 2 ] ! }
246- }
247- // 2. Handle HTTPS/URL-style, e.g. https://github.com/owner/repo.git
248- try {
249- const parsed = new URL ( remoteUrl )
250- const segments = parsed . pathname . split ( '/' )
251- const owner = segments . at ( - 2 )
252- const repo = segments . at ( - 1 ) ?. replace ( / \. g i t $ / , '' )
253- if ( owner && repo ) {
254- return { owner, repo }
255- }
256- } catch { }
257- debugFn ( 'error' , 'git: unmatched git remote URL format' )
258- debugDir ( 'inspect' , { remoteUrl } )
259- } catch ( e ) {
260- debugFn ( 'error' , 'caught: `git remote get-url origin` failed' )
261- debugDir ( 'inspect' , { error : e } )
262- }
263- return null
264- }
265-
266286export async function gitResetAndClean (
267287 branch = 'HEAD' ,
268288 cwd = process . cwd ( ) ,
@@ -307,3 +327,35 @@ export async function gitUnstagedModifiedFiles(
307327 }
308328 }
309329}
330+
331+ const parsedGitRemoteUrlCache = new Map < string , RepoInfo | null > ( )
332+
333+ export function parseGitRemoteUrl ( remoteUrl : string ) : RepoInfo | null {
334+ let result = parsedGitRemoteUrlCache . get ( remoteUrl ) ?? null
335+ if ( result ) {
336+ return { ...result }
337+ }
338+ // Handle SSH-style
339+ const sshMatch = / ^ g i t @ [ ^ : ] + : ( [ ^ / ] + ) \/ ( .+ ?) (?: \. g i t ) ? $ / . exec ( remoteUrl )
340+ // 1. Handle SSH-style, e.g. git@github.com:owner/repo.git
341+ if ( sshMatch ) {
342+ result = { owner : sshMatch [ 1 ] ! , repo : sshMatch [ 2 ] ! }
343+ }
344+ if ( result ) {
345+ // 2. Handle HTTPS/URL-style, e.g. https://github.com/owner/repo.git
346+ try {
347+ const parsed = new URL ( remoteUrl )
348+ // Remove leading slashes from pathname and split by "/" to extract segments.
349+ const segments = parsed . pathname . replace ( / ^ \/ + / , '' ) . split ( '/' )
350+ // The second-to-last segment is expected to be the owner (e.g., "owner" in /owner/repo.git).
351+ const owner = segments . at ( - 2 )
352+ // The last segment is expected to be the repo name, so we remove the ".git" suffix if present.
353+ const repo = segments . at ( - 1 ) ?. replace ( / \. g i t $ / , '' )
354+ if ( owner && repo ) {
355+ result = { owner, repo }
356+ }
357+ } catch { }
358+ }
359+ parsedGitRemoteUrlCache . set ( remoteUrl , result )
360+ return { ...result } as RepoInfo | null
361+ }
0 commit comments