All files / git / log.ts

65.38% Branches 17/26
61.54% Lines 24/39
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 
 
x2
 
 
 
 
 
x2
 
 
 
 
x4
x4
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x9
x10
x10
x9
 
 
 
 
 
 
 
 
 
 
 
 
 
x5
x4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 







I
























I


I


I


I







































// Imports
import type { Arrayable, Nullable } from "@libs/typing"
import { command } from "@libs/run/command"
export type { Arrayable, Nullable }

/**
 * Parse the output of `git log --pretty=<<%H>> <<%at>> <<%an>> %s`.
 */
export function log(sha: string, { stdout = "", ...options } = {} as LogOptions): LogEntry[] {
  if (!stdout) {
    ;({ stdout } = command("git", ["log", "--pretty=<<%H>> <<%at>> <<%an>> %s", `${sha}..${options?.head ?? ""}`], { sync: true, throw: true }))
  }
  // Parse entries
  let entries = []
  for (const line of stdout.trim().split("\n").filter(Boolean)) {
    try {
      const { sha, author, time, summary } = line.match(/^<<(?<sha>.{40})>> <<(?<time>\d+)>> <<(?<author>.*)>> (?<summary>.*)$/)!.groups!
      const match = summary.match(/^(?<type>[^\(\):]+)(?:\((?<scopes>[^\(\):]+)\))?(?<breaking>!?):\s+(?<subject>[\s\S]*)/)?.groups
      entries.push({
        sha,
        author,
        time: Number(time),
        conventional: !!match,
        type: match?.type ?? null,
        scopes: match?.scopes?.split(",").map((scope) => scope.trim()) ?? [],
        breaking: match ? !!match?.breaking : null,
        subject: match?.subject ?? summary,
        summary,
      })
    } catch (error) {
      throw new TypeError(`Failed to parse git log entry: "${line}" (${error})`)
    }
  }
  // Filter entries
  if (options?.filter?.conventional) {
    entries = entries.filter(({ conventional }) => conventional)
  }
  if (options?.filter?.breaking) {
    entries = entries.filter(({ breaking }) => breaking)
  }
  if (options?.filter?.types) {
    entries = entries.filter(({ type }) => [options?.filter?.types].filter(Boolean).flat().includes(type as string))
  }
  if (options?.filter?.scopes?.length) {
    entries = entries.filter(({ scopes }) => scopes.some((scope) => [options?.filter?.scopes].filter(Boolean).flat().includes(scope)))
  }
  return entries
}

/** Options for {@linkcode log()}. */
export type LogOptions = {
  /**
   * The output returned by `git log --pretty=<<%H>> <<%at>> <<%an>> %s`.
   *
   * If empty, the function will run `git log --pretty=<<%H>> <<%at>> <<%an>> %s` synchronously to populate this field.
   */
  stdout?: string
  /** The commit sha to compare against. */
  head?: string
  /** Filter the entries. */
  filter?: {
    /** Filter only conventional commits. */
    conventional?: boolean
    /** Filter by commit types (must follow conventional commits syntax). */
    types?: Arrayable<string>
    /** Filter by scopes (must follow conventional commits syntax). */
    scopes?: Arrayable<string>
    /** Filter by breaking changes (must follow conventional commits syntax). */
    breaking?: boolean
  }
}

/** Git log entry. */
export type LogEntry = {
  sha: string
  author: string
  time: number
  conventional: boolean
  type: Nullable<string>
  scopes: string[]
  breaking: Nullable<boolean>
  subject: string
  summary: string
}