-
Notifications
You must be signed in to change notification settings - Fork 65
Expand file tree
/
Copy pathpush-range.mts
More file actions
97 lines (85 loc) · 3.39 KB
/
Copy pathpush-range.mts
File metadata and controls
97 lines (85 loc) · 3.39 KB
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
85
86
87
88
89
90
91
92
93
94
95
96
97
// Pre-push commit-range computation. Resolves the `<base>..<local>` range the
// security gates scan for a given push line, handling new branches, force-pushes,
// and default-branch fallback. Returns undefined for skip cases (tags,
// deletions, no baseline).
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
import { debugCheck } from './check-output.mts'
import { git } from './git.mts'
const logger = getDefaultLogger()
export const ZERO_SHA = '0000000000000000000000000000000000000000'
// Computes the commit range to scan. Returns null if no scan needed
// (skip case — tag, delete, or no baseline).
export const computeRange = (
remote: string,
localRef: string,
localSha: string,
remoteSha: string,
): string | undefined => {
if (localRef.startsWith('refs/tags/')) {
debugCheck(`Skipping tag push: ${localRef}`)
return undefined
}
if (localSha === ZERO_SHA) {
return undefined
}
const refExists = (ref: string): boolean => {
const r = spawnSync('git', ['rev-parse', ref])
return r.status === 0
}
const defaultBranchOf = (remoteName: string): string => {
const sym = git('symbolic-ref', `refs/remotes/${remoteName}/HEAD`).trim()
if (sym) {
return sym.replace(`refs/remotes/${remoteName}/`, '')
}
// symbolic-ref unset (rare — happens with shallow clones, partial
// fetches, freshly-init'd remotes). Try main → master → 'main'
// per CLAUDE.md default-branch resolution. Reversing the order
// would mispick during rename migrations.
if (refExists(`${remoteName}/main`)) {
return 'main'
}
if (refExists(`${remoteName}/master`)) {
return 'master'
}
return 'main'
}
// git cat-file -e exits 0 silently on success; spawnSync directly
// so we can inspect status without printing.
const remoteShaExists = (sha: string): boolean => {
const result = spawnSync('git', ['cat-file', '-e', sha])
return result.status === 0
}
if (remoteSha === ZERO_SHA) {
// New branch — compare against remote default branch.
const def = defaultBranchOf(remote)
const baseRef = `${remote}/${def}`
if (!refExists(baseRef)) {
logger.warn('Skipping validation (no baseline to compare against)')
return undefined
}
return `${baseRef}..${localSha}`
}
const isAncestor = (ancestor: string, descendant: string): boolean =>
spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant])
.status === 0
// Existing branch.
if (!remoteShaExists(remoteSha) || !isAncestor(remoteSha, localSha)) {
// Force-push, history rewrite, or dangling object that is not an
// ancestor of the local tip — fall back to remote default branch.
//
// This base is wider than "new work": a history repair that reattaches an
// orphaned release tag puts already-published commits back in front of it.
// Gates whose only remedy is a rewrite subtract those via
// `resolveRewritableCommits` in ./push-release-tags.mts rather than
// demanding a rewrite that would re-orphan the tag.
const def = defaultBranchOf(remote)
const baseRef = `${remote}/${def}`
if (!refExists(baseRef)) {
logger.warn('Skipping validation (no baseline for force-push)')
return undefined
}
return `${baseRef}..${localSha}`
}
return `${remoteSha}..${localSha}`
}