• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

levibostian / new-deployment-tool / 14242840486

03 Apr 2025 12:22PM UTC coverage: 73.517% (+0.4%) from 73.132%
14242840486

Pull #48

github

web-flow
Merge d7324c618 into f105879dd
Pull Request #48: fix git hooks

110 of 123 branches covered (89.43%)

Branch coverage included in aggregate %.

316 of 423 new or added lines in 15 files covered. (74.7%)

11 existing lines in 4 files now uncovered.

745 of 1040 relevant lines covered (71.63%)

10.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.69
/lib/git.ts
1
import { exec } from "./exec.ts"
2
import { Exec } from "./exec.ts"
3
import { GitHubCommit } from "./github-api.ts"
4
import * as log from "./log.ts"
3✔
5

6
export interface Git {
7
  add: ({ exec, filePath }: { exec: Exec; filePath: string }) => Promise<void>
8
  commit: (
9
    { exec, message, dryRun }: { exec: Exec; message: string; dryRun: boolean },
10
  ) => Promise<GitHubCommit>
11
  push: (
12
    { exec, branch, forcePush, dryRun }: { exec: Exec; branch: string; forcePush: boolean; dryRun: boolean },
13
  ) => Promise<void>
14
  areAnyFilesStaged: ({ exec }: { exec: Exec }) => Promise<boolean>
15
  deleteBranch: (
16
    { exec, branch, dryRun }: { exec: Exec; branch: string; dryRun: boolean },
17
  ) => Promise<void>
18
  checkoutBranch: (
19
    { exec, branch, createBranchIfNotExist }: { exec: Exec; branch: string; createBranchIfNotExist: boolean },
20
  ) => Promise<void>
21
  doesLocalBranchExist: (
22
    { exec, branch }: { exec: Exec; branch: string },
23
  ) => Promise<boolean>
24
  merge: (
25
    { exec, branchToMergeIn, commitTitle, commitMessage, fastForwardOnly }: {
26
      exec: Exec
27
      branchToMergeIn: string
28
      commitTitle: string
29
      commitMessage: string
30
      fastForwardOnly?: boolean
31
    },
32
  ) => Promise<void>
33
  pull: ({ exec }: { exec: Exec }) => Promise<void>
34
  setUser: (
35
    { exec, name, email }: { exec: Exec; name: string; email: string },
36
  ) => Promise<void>
37
  squash: (
38
    { exec, branchToSquash, branchMergingInto, commitTitle, commitMessage }: {
39
      exec: Exec
40
      branchToSquash: string
41
      branchMergingInto: string
42
      commitTitle: string
43
      commitMessage: string
44
    },
45
  ) => Promise<void>
46
  rebase: (
47
    { exec, branchToRebaseOnto }: { exec: Exec; branchToRebaseOnto: string },
48
  ) => Promise<void>
49
  getLatestCommitsSince({ exec, commit }: { exec: Exec; commit: GitHubCommit }): Promise<GitHubCommit[]>
50
  getLatestCommitOnBranch({ exec, branch }: { exec: Exec; branch: string }): Promise<GitHubCommit>
51
  createLocalBranchFromRemote: ({ exec, branch }: { exec: Exec; branch: string }) => Promise<void>
52
}
53

54
const add = async (
3✔
55
  { exec, filePath }: { exec: Exec; filePath: string },
3✔
56
): Promise<void> => {
57
  await exec.run({
5✔
58
    command: `git add ${filePath}`,
5✔
59
    input: undefined,
5✔
60
  })
5✔
61
}
3✔
62

63
const commit = async (
3✔
64
  { exec, message, dryRun }: { exec: Exec; message: string; dryRun: boolean },
3✔
65
): Promise<GitHubCommit> => {
66
  if (await areAnyFilesStaged({ exec })) {
30✔
67
    // The author is the github actions bot.
68
    // Resources to find this author info:
69
    // https://github.com/orgs/community/discussions/26560
70
    // https://github.com/peter-evans/create-pull-request/blob/0c2a66fe4af462aa0761939bd32efbdd46592737/action.yml
71
    await exec.run({
15✔
72
      command: `git commit -m "${message}"${dryRun ? " --dry-run" : ""}`,
15✔
73
      input: undefined,
15✔
74
      envVars: {
15✔
75
        GIT_AUTHOR_NAME: "github-actions[bot]",
15✔
76
        GIT_COMMITTER_NAME: "github-actions[bot]",
15✔
77
        GIT_AUTHOR_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com",
15✔
78
        GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com",
15✔
79
      },
15✔
80
    })
15✔
81
  }
15✔
82

83
  return getLatestCommit({ exec })
48✔
84
}
3✔
85

86
const push = async (
3✔
87
  { exec, branch, forcePush, dryRun }: { exec: Exec; branch: string; forcePush: boolean; dryRun: boolean },
3✔
88
): Promise<void> => {
89
  const gitCommand = `git push origin ${branch}${forcePush ? " --force" : ""}`
7✔
90

91
  if (dryRun) {
7✔
92
    log.message(`[Dry Run] ${gitCommand}`)
8✔
93
    return
8✔
94
  }
8✔
95

96
  await exec.run({
10✔
97
    command: gitCommand,
10✔
98
    input: undefined,
10✔
99
  })
10✔
100
}
3✔
101

102
const areAnyFilesStaged = async (
3✔
103
  { exec }: { exec: Exec },
3✔
104
): Promise<boolean> => {
105
  const { stdout } = await exec.run({
13✔
106
    command: `git diff --cached --name-only`,
13✔
107
    input: undefined,
13✔
108
  })
13✔
109

110
  return stdout.trim() !== ""
21✔
111
}
3✔
112

113
const getLatestCommit = async (
3✔
114
  { exec }: { exec: Exec },
3✔
115
): Promise<GitHubCommit> => {
116
  const { stdout } = await exec.run({
9✔
117
    command: `git log -1 --pretty=format:"%H%n%s%n%ci"`,
9✔
118
    input: undefined,
9✔
119
  })
9✔
120

121
  const [sha, message, dateString] = stdout.trim().split("\n")
9✔
122

123
  return { sha, message, date: new Date(dateString) }
45✔
124
}
3✔
125

126
const deleteBranch = async (
3✔
127
  { exec, branch, dryRun }: { exec: Exec; branch: string; dryRun: boolean },
3✔
128
): Promise<void> => {
129
  const deleteLocalBranchCommand = `git branch -D ${branch}`
6✔
130

131
  if (dryRun) {
6✔
132
    log.message(`[Dry Run] ${deleteLocalBranchCommand}`)
7✔
133
  } else {
6✔
134
    await exec.run({
8✔
135
      command: deleteLocalBranchCommand,
8✔
136
      input: undefined,
8✔
137
    })
8✔
138
  }
9✔
139
}
3✔
140

141
const checkoutBranch = async (
3✔
142
  { exec, branch, createBranchIfNotExist }: { exec: Exec; branch: string; createBranchIfNotExist: boolean },
3✔
143
): Promise<void> => {
144
  await exec.run({
17✔
145
    command: `git checkout ${createBranchIfNotExist ? "-b " : ""}${branch}`,
17✔
146
    input: undefined,
17✔
147
  })
17✔
148
}
3✔
149

150
const doesLocalBranchExist = async (
3✔
151
  { exec, branch }: { exec: Exec; branch: string },
3✔
152
): Promise<boolean> => {
153
  const { exitCode } = await exec.run({
5✔
154
    command: `git show-ref --verify --quiet refs/heads/${branch}`,
5✔
155
    input: undefined,
5✔
156
    throwOnNonZeroExitCode: false,
5✔
157
  })
5✔
158

159
  return exitCode === 0
5✔
160
}
3✔
161

162
const merge = async (
3✔
163
  { exec, branchToMergeIn, commitTitle, commitMessage, fastForwardOnly }: {
3✔
164
    exec: Exec
165
    branchToMergeIn: string
166
    commitTitle: string
167
    commitMessage: string
168
    fastForwardOnly?: boolean
169
  },
3✔
170
): Promise<void> => {
171
  await exec.run({
6✔
172
    command: `git merge ${branchToMergeIn} -m "${commitTitle}" -m "${commitMessage}"${fastForwardOnly ? " --ff-only" : ""}`,
6✔
173
    input: undefined,
6✔
174
  })
6✔
175
}
3✔
176

177
const pull = async ({ exec }: { exec: Exec }): Promise<void> => {
3✔
178
  await exec.run({
9✔
179
    command: `git pull`,
9✔
180
    input: undefined,
9✔
181
  })
9✔
182
}
3✔
183

184
const setUser = async (
3✔
185
  { exec, name, email }: { exec: Exec; name: string; email: string },
3✔
186
): Promise<void> => {
187
  await exec.run({
6✔
188
    command: `git config user.name "${name}"`,
6✔
189
    input: undefined,
6✔
190
  })
6✔
191

192
  await exec.run({
6✔
193
    command: `git config user.email "${email}"`,
6✔
194
    input: undefined,
6✔
195
  })
6✔
196
}
3✔
197

198
// Squash all commits of a branch into 1 commit
199
const squash = async (
3✔
200
  { exec, branchToSquash, branchMergingInto, commitTitle, commitMessage }: {
3✔
201
    exec: Exec
202
    branchToSquash: string
203
    branchMergingInto: string
204
    commitTitle: string
205
    commitMessage: string
206
  },
3✔
207
): Promise<void> => {
208
  // We need to find out how many commits 1 branch is ahead of the other to find out how many unique commits there are.
209
  const { stdout } = await exec.run({
4✔
210
    command: `git rev-list --count ${branchMergingInto}..${branchToSquash}`,
4✔
211
    input: undefined,
4✔
212
  })
4✔
213

214
  const numberOfCommitsAheadOfBranchMergingInto = parseInt(stdout.trim())
4✔
215

216
  if (numberOfCommitsAheadOfBranchMergingInto === 0) {
×
NEW
217
    log.message(`Branches ${branchToSquash} and ${branchMergingInto} are already up to date. No commits to squash.`)
×
NEW
218
    return
×
UNCOV
219
  }
×
220

221
  // Now that we know how many commits are ahead, we can squash all of those commits into 1 commit.
222
  await exec.run({
4✔
223
    command: `git reset --soft HEAD~${numberOfCommitsAheadOfBranchMergingInto} && git commit -m "${commitTitle}" -m "${commitMessage}"`,
4✔
224
    input: undefined,
4✔
225
  })
4✔
226
}
3✔
227

228
const rebase = async (
3✔
229
  { exec, branchToRebaseOnto }: { exec: Exec; branchToRebaseOnto: string },
3✔
230
): Promise<void> => {
231
  await exec.run({
5✔
232
    command: `git rebase ${branchToRebaseOnto}`,
5✔
233
    input: undefined,
5✔
234
  })
5✔
235
}
3✔
236

237
const getLatestCommitsSince = async (
3✔
238
  { exec, commit }: { exec: Exec; commit: GitHubCommit },
3✔
239
): Promise<GitHubCommit[]> => {
240
  const { stdout } = await exec.run({
6✔
241
    command: `git log --pretty=format:"%H|%s|%ci" ${commit.sha}..HEAD`,
6✔
242
    input: undefined,
6✔
243
  })
6✔
244

245
  return stdout.trim().split("\n").map((commitString) => {
6✔
246
    const [sha, message, dateString] = commitString.split("|")
9✔
247

248
    return { sha, message, date: new Date(dateString) }
45✔
249
  })
6✔
250
}
3✔
251

252
const getLatestCommitOnBranch = async (
3✔
253
  { exec, branch }: { exec: Exec; branch: string },
3✔
254
): Promise<GitHubCommit> => {
255
  const { stdout } = await exec.run({
6✔
256
    command: `git log -1 --pretty=format:"%H|%s|%ci" ${branch}`,
6✔
257
    input: undefined,
6✔
258
  })
6✔
259

260
  const [sha, message, dateString] = stdout.trim().split("|")
6✔
261

262
  return { sha, message, date: new Date(dateString) }
30✔
263
}
3✔
264

265
/**
266
 * Makes sure that we have a local branch that has all of the commits that the remote branch has.
267
 *
268
 * There are a lot of commands here, just to get a local branch of a remote branch. After many attempts to simplify this, we would hit many different errors.
269
 * I believe that complexity comes because we run this tool on a CI server where the git config might be different.
270
 * Running all of these commands and running each command by itself (example: not running `git checkout -b` to try and combine creating a branch and checking it out)
271
 * have given the most consistent results.
272
 */
273
const createLocalBranchFromRemote = async (
3✔
274
  { exec, branch }: { exec: Exec; branch: string },
3✔
275
): Promise<void> => {
276
  const currentBranchName = (await exec.run({
5✔
277
    command: `git branch --show-current`,
5✔
278
    input: undefined,
5✔
279
  })).stdout.trim()
5✔
280

281
  // Perform a fetch, otherwise you might get errors about origin branch not being found.
282
  await exec.run({
6✔
283
    command: `git fetch origin`,
6✔
284
    input: undefined,
6✔
285
  })
6✔
286

287
  // Create a local branch that tracks the remote branch.
288
  await exec.run({
6✔
289
    command: `git branch --track ${branch} origin/${branch}`,
6✔
290
    input: undefined,
6✔
291
  })
6✔
292

293
  // Checkout the branch so we can pull it.
294
  await exec.run({
6✔
295
    command: `git checkout ${branch}`,
6✔
296
    input: undefined,
6✔
297
  })
6✔
298

299
  // Pull the branch from the remote.
300
  // Adding --no-rebase to avoid an error that could happen when you run pull.
301
  // The error is: You have divergent branches and need to specify how to reconcile them.
302
  await exec.run({
6✔
303
    command: `git pull --no-rebase origin ${branch}`,
6✔
304
    input: undefined,
6✔
305
  })
6✔
306

307
  // Switch back to the branch we were on before.
308
  await exec.run({
6✔
309
    command: `git checkout ${currentBranchName}`,
6✔
310
    input: undefined,
6✔
311
  })
6✔
312
}
3✔
313

314
export const git: Git = {
3✔
315
  add,
3✔
316
  commit,
3✔
317
  push,
3✔
318
  areAnyFilesStaged,
3✔
319
  deleteBranch,
3✔
320
  checkoutBranch,
3✔
321
  doesLocalBranchExist,
3✔
322
  merge,
3✔
323
  pull,
3✔
324
  setUser,
3✔
325
  squash,
3✔
326
  rebase,
3✔
327
  getLatestCommitsSince,
3✔
328
  getLatestCommitOnBranch,
3✔
329
  createLocalBranchFromRemote,
3✔
330
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc