/ src / utils / git.ts
git.ts
 1  import { memoize } from 'lodash-es'
 2  import { execFileNoThrow } from './execFileNoThrow.js'
 3  
 4  export const getIsGit = memoize(async (): Promise<boolean> => {
 5    const { code } = await execFileNoThrow('git', [
 6      'rev-parse',
 7      '--is-inside-work-tree',
 8    ])
 9    return code === 0
10  })
11  
12  export const getHead = async (): Promise<string> => {
13    const { stdout } = await execFileNoThrow('git', ['rev-parse', 'HEAD'])
14    return stdout.trim()
15  }
16  
17  export const getBranch = async (): Promise<string> => {
18    const { stdout } = await execFileNoThrow(
19      'git',
20      ['rev-parse', '--abbrev-ref', 'HEAD'],
21      undefined,
22      undefined,
23      false,
24    )
25    return stdout.trim()
26  }
27  
28  export const getRemoteUrl = async (): Promise<string | null> => {
29    // This might fail if there is no remote called origin
30    const { stdout, code } = await execFileNoThrow(
31      'git',
32      ['remote', 'get-url', 'origin'],
33      undefined,
34      undefined,
35      false,
36    )
37    return code === 0 ? stdout.trim() : null
38  }
39  
40  export const getIsHeadOnRemote = async (): Promise<boolean> => {
41    const { code } = await execFileNoThrow(
42      'git',
43      ['rev-parse', '@{u}'],
44      undefined,
45      undefined,
46      false,
47    )
48    return code === 0
49  }
50  
51  export const getIsClean = async (): Promise<boolean> => {
52    const { stdout } = await execFileNoThrow(
53      'git',
54      ['status', '--porcelain'],
55      undefined,
56      undefined,
57      false,
58    )
59    return stdout.trim().length === 0
60  }
61  
62  export interface GitRepoState {
63    commitHash: string
64    branchName: string
65    remoteUrl: string | null
66    isHeadOnRemote: boolean
67    isClean: boolean
68  }
69  
70  export async function getGitState(): Promise<GitRepoState | null> {
71    try {
72      const [commitHash, branchName, remoteUrl, isHeadOnRemote, isClean] =
73        await Promise.all([
74          getHead(),
75          getBranch(),
76          getRemoteUrl(),
77          getIsHeadOnRemote(),
78          getIsClean(),
79        ])
80  
81      return {
82        commitHash,
83        branchName,
84        remoteUrl,
85        isHeadOnRemote,
86        isClean,
87      }
88    } catch (_) {
89      // Fail silently - git state is best effort
90      return null
91    }
92  }