git-chopstick-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/examples/get-status.ts +84 -0
  4. package/package.json +20 -0
  5. package/src/git/add.ts +16 -0
  6. package/src/git/apply.ts +154 -0
  7. package/src/git/authentication.ts +19 -0
  8. package/src/git/branch.ts +206 -0
  9. package/src/git/checkout-index.ts +40 -0
  10. package/src/git/checkout.ts +235 -0
  11. package/src/git/cherry-pick.ts +504 -0
  12. package/src/git/clean.ts +9 -0
  13. package/src/git/clone.ts +86 -0
  14. package/src/git/coerce-to-buffer.ts +4 -0
  15. package/src/git/coerce-to-string.ts +4 -0
  16. package/src/git/commit.ts +136 -0
  17. package/src/git/config.ts +392 -0
  18. package/src/git/core.ts +625 -0
  19. package/src/git/create-tail-stream.ts +36 -0
  20. package/src/git/credential.ts +83 -0
  21. package/src/git/description.ts +33 -0
  22. package/src/git/diff-check.ts +27 -0
  23. package/src/git/diff-index.ts +116 -0
  24. package/src/git/diff.ts +880 -0
  25. package/src/git/environment.ts +116 -0
  26. package/src/git/exec.ts +285 -0
  27. package/src/git/fetch.ts +141 -0
  28. package/src/git/for-each-ref.ts +160 -0
  29. package/src/git/format-patch.ts +17 -0
  30. package/src/git/git-delimiter-parser.ts +95 -0
  31. package/src/git/gitignore.ts +157 -0
  32. package/src/git/index.ts +36 -0
  33. package/src/git/init.ts +11 -0
  34. package/src/git/interpret-trailers.ts +176 -0
  35. package/src/git/lfs.ts +100 -0
  36. package/src/git/log.ts +376 -0
  37. package/src/git/merge-tree.ts +42 -0
  38. package/src/git/merge.ts +154 -0
  39. package/src/git/multi-operation-terminal-output.ts +68 -0
  40. package/src/git/pull.ts +130 -0
  41. package/src/git/push-terminal-chunk.ts +41 -0
  42. package/src/git/push.ts +119 -0
  43. package/src/git/rebase.ts +627 -0
  44. package/src/git/reflog.ts +127 -0
  45. package/src/git/refs.ts +63 -0
  46. package/src/git/remote.ts +143 -0
  47. package/src/git/reorder.ts +153 -0
  48. package/src/git/reset.ts +101 -0
  49. package/src/git/rev-list.ts +201 -0
  50. package/src/git/rev-parse.ts +92 -0
  51. package/src/git/revert.ts +55 -0
  52. package/src/git/rm.ts +31 -0
  53. package/src/git/show.ts +88 -0
  54. package/src/git/spawn.ts +38 -0
  55. package/src/git/squash.ts +173 -0
  56. package/src/git/stage.ts +97 -0
  57. package/src/git/stash.ts +302 -0
  58. package/src/git/status.ts +502 -0
  59. package/src/git/submodule.ts +212 -0
  60. package/src/git/tag.ts +134 -0
  61. package/src/git/update-index.ts +169 -0
  62. package/src/git/update-ref.ts +50 -0
  63. package/src/git/var.ts +42 -0
  64. package/src/git/worktree-include.ts +146 -0
  65. package/src/git/worktree.ts +219 -0
  66. package/src/lib/api.ts +7 -0
  67. package/src/lib/diff-parser.ts +249 -0
  68. package/src/lib/directory-exists.ts +10 -0
  69. package/src/lib/errno-exception.ts +12 -0
  70. package/src/lib/fatal-error.ts +23 -0
  71. package/src/lib/feature-flag.ts +29 -0
  72. package/src/lib/file-system.ts +7 -0
  73. package/src/lib/get-old-path.ts +11 -0
  74. package/src/lib/git/environment.ts +14 -0
  75. package/src/lib/git-perf.ts +3 -0
  76. package/src/lib/helpers/default-branch.ts +3 -0
  77. package/src/lib/helpers/path.ts +5 -0
  78. package/src/lib/hooks/with-hooks-env.ts +7 -0
  79. package/src/lib/merge.ts +3 -0
  80. package/src/lib/noop.ts +1 -0
  81. package/src/lib/patch-formatter.ts +18 -0
  82. package/src/lib/path-exists.ts +7 -0
  83. package/src/lib/progress/from-process.ts +10 -0
  84. package/src/lib/progress/index.ts +43 -0
  85. package/src/lib/progress/revert.ts +17 -0
  86. package/src/lib/rebase.ts +3 -0
  87. package/src/lib/remove-remote-prefix.ts +4 -0
  88. package/src/lib/resolve-git-proxy.ts +3 -0
  89. package/src/lib/round.ts +4 -0
  90. package/src/lib/split-buffer.ts +14 -0
  91. package/src/lib/status-parser.ts +188 -0
  92. package/src/lib/stores/helpers/find-default-remote.ts +3 -0
  93. package/src/lib/trampoline/trampoline-environment.ts +8 -0
  94. package/src/models/branch.ts +78 -0
  95. package/src/models/cherry-pick.ts +12 -0
  96. package/src/models/clone-options.ts +6 -0
  97. package/src/models/commit-identity.ts +35 -0
  98. package/src/models/commit.ts +44 -0
  99. package/src/models/computed-action.ts +6 -0
  100. package/src/models/diff/diff-data.ts +78 -0
  101. package/src/models/diff/diff-line.ts +36 -0
  102. package/src/models/diff/diff-selection.ts +165 -0
  103. package/src/models/diff/image-diff.ts +6 -0
  104. package/src/models/diff/image.ts +8 -0
  105. package/src/models/diff/index.ts +6 -0
  106. package/src/models/diff/raw-diff.ts +41 -0
  107. package/src/models/git-author.ts +16 -0
  108. package/src/models/manual-conflict-resolution.ts +4 -0
  109. package/src/models/merge.ts +6 -0
  110. package/src/models/multi-commit-operation.ts +6 -0
  111. package/src/models/progress.ts +67 -0
  112. package/src/models/rebase.ts +20 -0
  113. package/src/models/remote.ts +10 -0
  114. package/src/models/repository.ts +16 -0
  115. package/src/models/stash-entry.ts +25 -0
  116. package/src/models/status.ts +275 -0
  117. package/src/models/submodule.ts +13 -0
  118. package/src/models/worktree.ts +11 -0
  119. package/tsconfig.json +17 -0
@@ -0,0 +1,17 @@
1
+ import { revRange } from './rev-list'
2
+ import { Repository } from '../models/repository'
3
+ import { git } from '.'
4
+
5
+ /**
6
+ * Generate a patch representing the changes associated with a range of commits
7
+ *
8
+ * @param repository where to generate path from
9
+ * @param base starting commit in range
10
+ * @param head ending commit in rage
11
+ * @returns patch generated
12
+ */
13
+ export function formatPatch({ path }: Repository, base: string, head: string) {
14
+ const range = revRange(base, head)
15
+ const args = ['format-patch', '--unified=1', '--minimal', '--stdout', range]
16
+ return git(args, path, 'formatPatch').then(x => x.stdout)
17
+ }
@@ -0,0 +1,95 @@
1
+ import { splitBuffer } from '../lib/split-buffer'
2
+
3
+ /**
4
+ * Create a new parser suitable for parsing --format output from commands such
5
+ * as `git log`, `git stash`, and other commands that are not derived from
6
+ * `ref-filter`.
7
+ *
8
+ * Returns an object with the arguments that need to be appended to the git
9
+ * call and the parse function itself
10
+ *
11
+ * @param fields An object keyed on the friendly name of the value being
12
+ * parsed with the value being the format string of said value.
13
+ *
14
+ * Example:
15
+ *
16
+ * `const { args, parse } = createLogParser({ sha: '%H' })`
17
+ */
18
+ export function createLogParser<T extends Record<string, string>>(fields: T) {
19
+ const keys: Array<keyof T> = Object.keys(fields)
20
+ const format = Object.values(fields).join('%x00')
21
+ const formatArgs = ['-z', `--format=${format}`]
22
+
23
+ const parse = <V extends string | Buffer>(value: V) => {
24
+ const records = (
25
+ Buffer.isBuffer(value) ? splitBuffer(value, '\0') : value.split('\0')
26
+ ) as V[]
27
+ const entries = []
28
+
29
+ for (let i = 0; i < records.length - keys.length; i += keys.length) {
30
+ const entry = {} as { [K in keyof T]: V }
31
+ keys.forEach((key, ix) => (entry[key] = records[i + ix]))
32
+ entries.push(entry)
33
+ }
34
+
35
+ return entries
36
+ }
37
+
38
+ return { formatArgs, parse }
39
+ }
40
+
41
+ /**
42
+ * Create a new parser suitable for parsing --format output from commands such
43
+ * as `git for-each-ref`, `git branch`, and other commands that are not derived
44
+ * from `git log`.
45
+ *
46
+ * Returns an object with the arguments that need to be appended to the git
47
+ * call and the parse function itself
48
+ *
49
+ * @param fields An object keyed on the friendly name of the value being
50
+ * parsed with the value being the format string of said value.
51
+ *
52
+ * Example:
53
+ *
54
+ * `const { args, parse } = createForEachRefParser({ sha: '%(objectname)' })`
55
+ */
56
+ export function createForEachRefParser<T extends Record<string, string>>(
57
+ fields: T
58
+ ) {
59
+ const keys: Array<keyof T> = Object.keys(fields)
60
+ const format = Object.values(fields).join('%00')
61
+ const formatArgs = [`--format=%00${format}%00`]
62
+
63
+ const parse = (value: string) => {
64
+ const records = value.split('\0')
65
+ const entries = new Array<{ [K in keyof T]: string }>()
66
+
67
+ let entry
68
+ let consumed = 0
69
+
70
+ // start at 1 to avoid 0 modulo X problem. The first record is guaranteed
71
+ // to be empty anyway (due to %00 at the start of --format)
72
+ for (let i = 1; i < records.length - 1; i++) {
73
+ if (i % (keys.length + 1) === 0) {
74
+ if (records[i] !== '\n') {
75
+ throw new Error('Expected newline')
76
+ }
77
+ continue
78
+ }
79
+
80
+ entry = entry ?? ({} as { [K in keyof T]: string })
81
+ const key = keys[consumed % keys.length]
82
+ entry[key] = records[i]
83
+ consumed++
84
+
85
+ if (consumed % keys.length === 0) {
86
+ entries.push(entry)
87
+ entry = undefined
88
+ }
89
+ }
90
+
91
+ return entries
92
+ }
93
+
94
+ return { formatArgs, parse }
95
+ }
@@ -0,0 +1,157 @@
1
+ import * as Path from 'path'
2
+ import * as FS from 'fs'
3
+ import { Repository } from '../models/repository'
4
+ import { getConfigValue } from './config'
5
+ import { writeFile } from 'fs/promises'
6
+
7
+ /**
8
+ * Read the contents of the repository .gitignore.
9
+ *
10
+ * Returns a promise which will either be rejected or resolved
11
+ * with the contents of the file. If there's no .gitignore file
12
+ * in the repository root the promise will resolve with null.
13
+ */
14
+ export async function readGitIgnoreAtRoot(
15
+ repository: Repository
16
+ ): Promise<string | null> {
17
+ const ignorePath = Path.join(repository.path, '.gitignore')
18
+
19
+ return new Promise<string | null>((resolve, reject) => {
20
+ FS.readFile(ignorePath, 'utf8', (err, data) => {
21
+ if (err) {
22
+ if (err.code === 'ENOENT') {
23
+ resolve(null)
24
+ } else {
25
+ reject(err)
26
+ }
27
+ } else {
28
+ resolve(data)
29
+ }
30
+ })
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Persist the given content to the repository root .gitignore.
36
+ *
37
+ * If the repository root doesn't contain a .gitignore file one
38
+ * will be created, otherwise the current file will be overwritten.
39
+ */
40
+ export async function saveGitIgnore(
41
+ repository: Repository,
42
+ text: string
43
+ ): Promise<void> {
44
+ const ignorePath = Path.join(repository.path, '.gitignore')
45
+
46
+ if (text === '') {
47
+ return new Promise<void>((resolve, reject) => {
48
+ FS.unlink(ignorePath, err => {
49
+ if (err) {
50
+ reject(err)
51
+ } else {
52
+ resolve()
53
+ }
54
+ })
55
+ })
56
+ }
57
+
58
+ const fileContents = await formatGitIgnoreContents(text, repository)
59
+ await writeFile(ignorePath, fileContents)
60
+ }
61
+
62
+ /** Add the given pattern or patterns to the root gitignore file */
63
+ export async function appendIgnoreRule(
64
+ repository: Repository,
65
+ patterns: string | string[]
66
+ ): Promise<void> {
67
+ const text = (await readGitIgnoreAtRoot(repository)) || ''
68
+
69
+ const currentContents = await formatGitIgnoreContents(text, repository)
70
+
71
+ const newPatternText =
72
+ patterns instanceof Array ? patterns.join('\n') : patterns
73
+ const newText = await formatGitIgnoreContents(
74
+ `${currentContents}${newPatternText}`,
75
+ repository
76
+ )
77
+
78
+ await saveGitIgnore(repository, newText)
79
+ }
80
+
81
+ /**
82
+ * Convenience method to add the given file path(s) to the repository's gitignore.
83
+ *
84
+ * The file path will be escaped before adding.
85
+ */
86
+ export async function appendIgnoreFile(
87
+ repository: Repository,
88
+ filePath: string | string[]
89
+ ): Promise<void> {
90
+ if (filePath instanceof Array) {
91
+ const escapedFilePaths = filePath.map(path =>
92
+ escapeGitSpecialCharacters(path)
93
+ )
94
+
95
+ return appendIgnoreRule(repository, escapedFilePaths)
96
+ }
97
+
98
+ const escapedFilePath = escapeGitSpecialCharacters(filePath)
99
+ return appendIgnoreRule(repository, escapedFilePath)
100
+ }
101
+
102
+ /** Escapes a string from special characters used in a gitignore file */
103
+ export function escapeGitSpecialCharacters(pattern: string): string {
104
+ const specialCharacters = /[\[\]!\*\#\?]/g
105
+
106
+ return pattern.replaceAll(specialCharacters, match => {
107
+ return '\\' + match
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Format the gitignore text based on the current config settings.
113
+ *
114
+ * This setting looks at core.autocrlf to decide which line endings to use
115
+ * when updating the .gitignore file.
116
+ *
117
+ * If core.safecrlf is also set, adding this file to the index may cause
118
+ * Git to return a non-zero exit code, leaving the working directory in a
119
+ * confusing state for the user. So we should reformat the file in that
120
+ * case.
121
+ *
122
+ * @param text The text to format.
123
+ * @param repository The repository associated with the gitignore file.
124
+ */
125
+ async function formatGitIgnoreContents(
126
+ text: string,
127
+ repository: Repository
128
+ ): Promise<string> {
129
+ const autocrlf = await getConfigValue(repository, 'core.autocrlf')
130
+ const safecrlf = await getConfigValue(repository, 'core.safecrlf')
131
+
132
+ return new Promise<string>((resolve, reject) => {
133
+ if (autocrlf === 'true' && safecrlf === 'true') {
134
+ // based off https://stackoverflow.com/a/141069/1363815
135
+ const normalizedText = text.replace(/\r\n|\n\r|\n|\r/g, '\r\n')
136
+ resolve(normalizedText + '\r\n')
137
+ return
138
+ }
139
+
140
+ if (text === '' || text.endsWith('\n')) {
141
+ resolve(text)
142
+ return
143
+ }
144
+
145
+ if (autocrlf == null) {
146
+ // fallback to Git default behaviour
147
+ resolve(`${text}\n`)
148
+ } else {
149
+ const linesEndInCRLF = autocrlf === 'true'
150
+ if (linesEndInCRLF) {
151
+ resolve(`${text}\n`)
152
+ } else {
153
+ resolve(`${text}\r\n`)
154
+ }
155
+ }
156
+ })
157
+ }
@@ -0,0 +1,36 @@
1
+ export * from './apply'
2
+ export * from './branch'
3
+ export * from './checkout'
4
+ export * from './clone'
5
+ export * from './commit'
6
+ export * from './config'
7
+ export * from './core'
8
+ export * from './description'
9
+ export * from './diff'
10
+ export * from './fetch'
11
+ export * from './for-each-ref'
12
+ export * from './init'
13
+ export * from './log'
14
+ export * from './pull'
15
+ export * from './push'
16
+ export * from './reflog'
17
+ export * from './refs'
18
+ export * from './remote'
19
+ export * from './reset'
20
+ export * from './rev-list'
21
+ export * from './rev-parse'
22
+ export * from './status'
23
+ export * from './update-ref'
24
+ export * from './var'
25
+ export * from './merge'
26
+ export * from './diff-index'
27
+ export * from './checkout-index'
28
+ export * from './revert'
29
+ export * from './rm'
30
+ export * from './submodule'
31
+ export * from './interpret-trailers'
32
+ export * from './gitignore'
33
+ export * from './rebase'
34
+ export * from './format-patch'
35
+ export * from './tag'
36
+ export * from './worktree'
@@ -0,0 +1,11 @@
1
+ import { getDefaultBranch } from '../lib/helpers/default-branch'
2
+ import { git } from './core'
3
+
4
+ /** Init a new git repository in the given path. */
5
+ export async function initGitRepository(path: string): Promise<void> {
6
+ await git(
7
+ ['-c', `init.defaultBranch=${await getDefaultBranch()}`, 'init'],
8
+ path,
9
+ 'initGitRepository'
10
+ )
11
+ }
@@ -0,0 +1,176 @@
1
+ import { git } from './core'
2
+ import { Repository } from '../models/repository'
3
+ import { getConfigValue } from './config'
4
+
5
+ /**
6
+ * A representation of a Git commit message trailer.
7
+ *
8
+ * See git-interpret-trailers for more information.
9
+ */
10
+ export interface ITrailer {
11
+ readonly token: string
12
+ readonly value: string
13
+ }
14
+
15
+ /**
16
+ * Gets a value indicating whether the trailer token is
17
+ * Co-Authored-By. Does not validate the token value.
18
+ */
19
+ export function isCoAuthoredByTrailer(trailer: ITrailer) {
20
+ return trailer.token.toLowerCase() === 'co-authored-by'
21
+ }
22
+
23
+ /**
24
+ * Parse a string containing only unfolded trailers produced by
25
+ * git-interpret-trailers --only-input --only-trailers --unfold or
26
+ * a derivative such as git log --format="%(trailers:only,unfold)"
27
+ *
28
+ * @param trailers A string containing one well formed trailer per
29
+ * line
30
+ *
31
+ * @param separators A string containing all characters to use when
32
+ * attempting to find the separator between token
33
+ * and value in a trailer. See the configuration
34
+ * option trailer.separators for more information
35
+ *
36
+ * Also see getTrailerSeparatorCharacters.
37
+ */
38
+ export function parseRawUnfoldedTrailers(trailers: string, separators: string) {
39
+ const lines = trailers.split('\n')
40
+ const parsedTrailers = new Array<ITrailer>()
41
+
42
+ for (const line of lines) {
43
+ const trailer = parseSingleUnfoldedTrailer(line, separators)
44
+
45
+ if (trailer) {
46
+ parsedTrailers.push(trailer)
47
+ }
48
+ }
49
+
50
+ return parsedTrailers
51
+ }
52
+
53
+ export function parseSingleUnfoldedTrailer(
54
+ line: string,
55
+ separators: string
56
+ ): ITrailer | null {
57
+ for (const separator of separators) {
58
+ const ix = line.indexOf(separator)
59
+ if (ix > 0) {
60
+ return {
61
+ token: line.substring(0, ix).trim(),
62
+ value: line.substring(ix + 1).trim(),
63
+ }
64
+ }
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * Get a string containing the characters that may be used in this repository
72
+ * separate tokens from values in commit message trailers. If no specific
73
+ * trailer separator is configured the default separator (:) will be returned.
74
+ */
75
+ export async function getTrailerSeparatorCharacters(
76
+ repository: Repository
77
+ ): Promise<string> {
78
+ return (await getConfigValue(repository, 'trailer.separators')) || ':'
79
+ }
80
+
81
+ /**
82
+ * Extract commit message trailers from a commit message.
83
+ *
84
+ * The trailers returned here are unfolded, i.e. they've had their
85
+ * whitespace continuation removed and are all on one line. See the
86
+ * documentation for --unfold in the help for `git interpret-trailers`
87
+ *
88
+ * @param repository The repository in which to run the interpret-
89
+ * trailers command. Although not intuitive this
90
+ * does matter as there are configuration options
91
+ * available for the format, position, etc of commit
92
+ * message trailers. See the manpage for
93
+ * git-interpret-trailers for more information.
94
+ *
95
+ * @param commitMessage A commit message from where to attempt to extract
96
+ * commit message trailers.
97
+ *
98
+ * @returns An array of zero or more parsed trailers
99
+ */
100
+ export async function parseTrailers(
101
+ repository: Repository,
102
+ commitMessage: string
103
+ ): Promise<ReadonlyArray<ITrailer>> {
104
+ const result = await git(
105
+ ['interpret-trailers', '--parse'],
106
+ repository.path,
107
+ 'parseTrailers',
108
+ {
109
+ stdin: commitMessage,
110
+ }
111
+ )
112
+
113
+ const trailers = result.stdout
114
+
115
+ if (trailers.length === 0) {
116
+ return []
117
+ }
118
+
119
+ const separators = await getTrailerSeparatorCharacters(repository)
120
+ return parseRawUnfoldedTrailers(result.stdout, separators)
121
+ }
122
+
123
+ /**
124
+ * Merge one or more commit message trailers into a commit message.
125
+ *
126
+ * If no trailers are given this method will simply try to ensure that
127
+ * any trailers that happen to be part of the raw message are formatted
128
+ * in accordance with the configuration options set for trailers in
129
+ * the given repository.
130
+ *
131
+ * Note that configuration may be set so that duplicate trailers are
132
+ * kept or discarded.
133
+ *
134
+ * @param repository The repository in which to run the interpret-
135
+ * trailers command. Although not intuitive this
136
+ * does matter as there are configuration options
137
+ * available for the format, position, etc of commit
138
+ * message trailers. See the manpage for
139
+ * git-interpret-trailers for more information.
140
+ *
141
+ * @param commitMessage A commit message with or without existing commit
142
+ * message trailers into which to merge the trailers
143
+ * given in the trailers parameter
144
+ *
145
+ * @param trailers Zero or more trailers to merge into the commit message
146
+ *
147
+ * @returns A commit message string where the provided trailers (if)
148
+ * any have been merged into the commit message using the
149
+ * configuration settings for trailers in the provided
150
+ * repository.
151
+ */
152
+ export async function mergeTrailers(
153
+ repository: Repository,
154
+ commitMessage: string,
155
+ trailers: ReadonlyArray<ITrailer>,
156
+ unfold: boolean = false
157
+ ) {
158
+ const args = ['interpret-trailers']
159
+
160
+ // See https://github.com/git/git/blob/ebf3c04b262aa/Documentation/git-interpret-trailers.txt#L129-L132
161
+ args.push('--no-divider')
162
+
163
+ if (unfold) {
164
+ args.push('--unfold')
165
+ }
166
+
167
+ for (const trailer of trailers) {
168
+ args.push('--trailer', `${trailer.token}=${trailer.value}`)
169
+ }
170
+
171
+ const result = await git(args, repository.path, 'mergeTrailers', {
172
+ stdin: commitMessage,
173
+ })
174
+
175
+ return result.stdout
176
+ }
package/src/git/lfs.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { git } from './core'
2
+ import { Repository } from '../models/repository'
3
+
4
+ /** Install the global LFS filters. */
5
+ export async function installGlobalLFSFilters(force: boolean): Promise<void> {
6
+ const args = ['lfs', 'install', '--skip-repo']
7
+ if (force) {
8
+ args.push('--force')
9
+ }
10
+
11
+ await git(args, __dirname, 'installGlobalLFSFilter')
12
+ }
13
+
14
+ /** Install LFS hooks in the repository. */
15
+ export async function installLFSHooks(
16
+ repository: Repository,
17
+ force: boolean
18
+ ): Promise<void> {
19
+ const args = ['lfs', 'install']
20
+ if (force) {
21
+ args.push('--force')
22
+ }
23
+
24
+ await git(args, repository.path, 'installLFSHooks')
25
+ }
26
+
27
+ /** Is the repository configured to track any paths with LFS? */
28
+ export async function isUsingLFS(repository: Repository): Promise<boolean> {
29
+ const env = {
30
+ GIT_LFS_TRACK_NO_INSTALL_HOOKS: '1',
31
+ }
32
+ const result = await git(['lfs', 'track'], repository.path, 'isUsingLFS', {
33
+ env,
34
+ })
35
+ return result.stdout.length > 0
36
+ }
37
+
38
+ /**
39
+ * Check if a provided file path is being tracked by Git LFS
40
+ *
41
+ * This uses the Git plumbing to read the .gitattributes file
42
+ * for any LFS-related rules that are set for the file
43
+ *
44
+ * @param repository repository with
45
+ * @param path relative file path in the repository
46
+ */
47
+ export async function isTrackedByLFS(
48
+ repository: Repository,
49
+ path: string
50
+ ): Promise<boolean> {
51
+ const { stdout } = await git(
52
+ ['check-attr', 'filter', path],
53
+ repository.path,
54
+ 'checkAttrForLFS'
55
+ )
56
+
57
+ // "git check-attr -a" will output every filter it can find in .gitattributes
58
+ // and it looks like this:
59
+ //
60
+ // README.md: diff: lfs
61
+ // README.md: merge: lfs
62
+ // README.md: text: unset
63
+ // README.md: filter: lfs
64
+ //
65
+ // To verify git-lfs this test will just focus on that last row, "filter",
66
+ // and the value associated with it. If nothing is found in .gitattributes
67
+ // the output will look like this
68
+ //
69
+ // README.md: filter: unspecified
70
+
71
+ const lfsFilterRegex = /: filter: lfs/
72
+
73
+ const match = lfsFilterRegex.exec(stdout)
74
+
75
+ return match !== null
76
+ }
77
+
78
+ /**
79
+ * Query a Git repository and filter the set of provided relative paths to see
80
+ * which are not covered by the current Git LFS configuration.
81
+ *
82
+ * @param repository
83
+ * @param filePaths List of relative paths in the repository
84
+ */
85
+ export async function filesNotTrackedByLFS(
86
+ repository: Repository,
87
+ filePaths: ReadonlyArray<string>
88
+ ): Promise<ReadonlyArray<string>> {
89
+ const filesNotTrackedByGitLFS = new Array<string>()
90
+
91
+ for (const file of filePaths) {
92
+ const isTracked = await isTrackedByLFS(repository, file)
93
+
94
+ if (!isTracked) {
95
+ filesNotTrackedByGitLFS.push(file)
96
+ }
97
+ }
98
+
99
+ return filesNotTrackedByGitLFS
100
+ }