-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommit.sh
More file actions
executable file
·494 lines (440 loc) · 20 KB
/
commit.sh
File metadata and controls
executable file
·494 lines (440 loc) · 20 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
#!/usr/bin/env bash
#
# DCO-enabled Git Commit Wrapper
# ===============================
# This script wraps `git commit` to automatically add --signoff
# and shows the DCO terms on first use.
#
set -e
# Color codes for better UX
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly MAGENTA='\033[0;35m'
readonly NC='\033[0m' # No Color
readonly BOLD='\033[1m'
# Verbose logging
VERBOSE=false
# SSH signing key path (optional, set via --signing-key)
SIGNING_KEY=""
# Build git commit args with optional SSH signing
git_commit_with_sign() {
local sign_args=()
if [[ -n "$SIGNING_KEY" ]]; then
sign_args+=(-c 'gpg.format=ssh' -c "user.signingkey=$SIGNING_KEY")
fi
git "${sign_args[@]}" commit "$@"
}
verbose_log() {
if [[ "$VERBOSE" == "true" ]]; then
echo -e "${BLUE}[verbose]${NC} $*" >&2
fi
}
# Resolve package version from package.json next to this script
get_package_version() {
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$script_dir/package.json" ]]; then
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$script_dir/package.json" | head -1
else
echo "unknown"
fi
}
readonly DCO_VERSION=$(get_package_version)
# Find the git repository root
find_git_root() {
local dir
dir=$(git rev-parse --show-toplevel 2>/dev/null) || {
echo -e "${RED}Error: Not in a git repository${NC}" >&2
exit 1
}
echo "$dir"
}
# Record DCO signature in git tree for permanent record
record_dco_signature() {
local git_root="$1"
local name="$2"
local email="$3"
local date="$4"
local agreement_commit="$5"
local agreement_change_date="$6"
local sig_file="$git_root/.dco-signatures"
# Create or append to the signatures file
if [[ ! -f "$sig_file" ]]; then
cat > "$sig_file" <<EOF
# DCO Signatures
This file contains a permanent record of all Developer Certificate of Origin (DCO) agreements.
Each contributor who has agreed to the DCO.md is listed below with their signing date.
Format: name <email> | signed: <date> | agreement: <commit> (<agreement_change_date>) [| signature: <key_fingerprint>]
---
EOF
fi
# Append the new signature (single line with all info)
local sig_line="$name <$email> | signed: $date | agreement: $agreement_commit ($agreement_change_date)"
if [[ -n "$SIGNING_KEY" ]]; then
local fingerprint
fingerprint=$(ssh-keygen -lf "$SIGNING_KEY" 2>/dev/null | awk '{print $2}')
if [[ -n "$fingerprint" ]]; then
sig_line="$sig_line | signature: $fingerprint"
fi
fi
echo "$sig_line" >> "$sig_file"
# Commit the signature to git
# Save list of currently staged files (excluding .dco-signatures)
local staged_files
staged_files=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null | grep -v "^\.dco-signatures$" || true)
verbose_log "record_dco_signature: staged files to preserve: $(echo "$staged_files" | tr '\n' ' ')"
# Unstage everything
git reset >/dev/null 2>&1 || true
# Stage ONLY the signature file
git add "$sig_file" 2>/dev/null || true
# Create a commit for the DCO signature (should only contain .dco-signatures)
local sign_args=()
if [[ -n "$SIGNING_KEY" ]]; then
sign_args+=(--gpg-sign)
fi
if git_commit_with_sign --signoff --no-verify "${sign_args[@]}" -m "[DCO] DCO.md signed by $name" -m "Developer Certificates of Origin established using https://github.com/Stream44/dco" >/dev/null 2>&1; then
echo -e "${GREEN}✓ Signature recorded in repository history${NC}"
else
echo -e "${YELLOW}Note: Signature file already up to date${NC}"
fi
# Restore the originally staged files
if [[ -n "$staged_files" ]]; then
echo "$staged_files" | while IFS= read -r file; do
git add "$file" 2>/dev/null || true
done
fi
}
# Show the DCO and get user agreement
show_dco_first_time() {
local git_root="$1"
local auto_agree="$2"
local dco_file="$git_root/DCO.md"
local marker_file="$git_root/.git/.dco-agreed"
# Check if user has already agreed
if [[ -f "$marker_file" ]]; then
# Verify the current git user matches the one who signed
local current_name current_email
current_name=$(git config user.name 2>/dev/null || echo "")
current_email=$(git config user.email 2>/dev/null || echo "")
local stored_name stored_email stored_date stored_agreement_commit stored_agreement_change_date
stored_name=$(grep "^name=" "$marker_file" | cut -d'=' -f2-)
stored_email=$(grep "^email=" "$marker_file" | cut -d'=' -f2-)
stored_date=$(grep "^date=" "$marker_file" | cut -d'=' -f2-)
stored_agreement_commit=$(grep "^agreement_commit=" "$marker_file" | cut -d'=' -f2-)
stored_agreement_change_date=$(grep "^agreement_change_date=" "$marker_file" | cut -d'=' -f2-)
if [[ "$current_name" != "$stored_name" ]] || [[ "$current_email" != "$stored_email" ]]; then
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
echo -e "${RED}${BOLD} DCO Identity Mismatch${NC}" >&2
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
echo >&2
echo -e "${YELLOW}The DCO was originally signed by:${NC}" >&2
echo -e " Name: ${CYAN}$stored_name${NC}" >&2
echo -e " Email: ${CYAN}$stored_email${NC}" >&2
echo >&2
echo -e "${YELLOW}But you are currently configured as:${NC}" >&2
echo -e " Name: ${CYAN}$current_name${NC}" >&2
echo -e " Email: ${CYAN}$current_email${NC}" >&2
echo >&2
echo -e "${BLUE}To sign with a different identity, please remove the DCO agreement file:${NC}" >&2
echo -e " ${BOLD}rm $marker_file${NC}" >&2
echo >&2
echo -e "${BLUE}Then run this script again to sign the DCO with your current identity.${NC}" >&2
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2
exit 1
fi
# Verify the signature exists in .dco-signatures file
local sig_file="$git_root/.dco-signatures"
local sig_valid="false"
if [[ -f "$sig_file" ]]; then
local sig_line
sig_line=$(grep "$stored_name.*<$stored_email>" "$sig_file" || true)
if [[ -n "$sig_line" ]]; then
sig_valid="true"
# Re-create marker from signature line if marker data is stale/empty
if [[ -z "$stored_agreement_commit" ]]; then
verbose_log "Marker missing agreement data, restoring from .dco-signatures"
local restored_agreement_commit restored_agreement_change_date restored_signed_date
restored_agreement_commit=$(echo "$sig_line" | sed -n 's/.*| agreement: \([a-f0-9]*\).*/\1/p')
restored_agreement_change_date=$(echo "$sig_line" | sed -n 's/.*| agreement: [a-f0-9]* (\(.*\))/\1/p')
restored_signed_date=$(echo "$sig_line" | sed -n 's/.*| signed: \([^|]*\) |.*/\1/p')
cat > "$marker_file" <<EOF
name=$stored_name
email=$stored_email
date=$restored_signed_date
agreement_commit=$restored_agreement_commit
agreement_change_date=$restored_agreement_change_date
EOF
stored_date="$restored_signed_date"
stored_agreement_commit="$restored_agreement_commit"
stored_agreement_change_date="$restored_agreement_change_date"
echo -e "${GREEN}✓ DCO marker restored from existing signature${NC}"
fi
else
verbose_log "Signature not found in .dco-signatures, will re-sign"
fi
else
verbose_log ".dco-signatures missing, will re-sign"
fi
if [[ "$sig_valid" == "false" ]]; then
# Auto-recover: remove stale marker and fall through to re-sign
echo -e "${YELLOW}DCO signature record missing or out of sync. Re-signing...${NC}"
rm -f "$marker_file"
# Fall through to the signing flow below
else
# Already signed - display details
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}${BOLD} DCO Already Signed${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo
echo -e " ${BOLD}Signer:${NC} $stored_name <$stored_email>"
echo -e " ${BOLD}Signed:${NC} $stored_date"
if [[ -n "$stored_agreement_commit" ]]; then
echo -e " ${BOLD}Agreement commit:${NC} $(git rev-parse --short "$stored_agreement_commit" 2>/dev/null || echo "$stored_agreement_commit")"
fi
if [[ -n "$stored_agreement_change_date" ]]; then
echo -e " ${BOLD}Agreement date:${NC} $stored_agreement_change_date"
fi
echo
return 0
fi
fi
# No marker file — check if .dco-signatures already has this user's signature
local sig_file="$git_root/.dco-signatures"
if [[ -f "$sig_file" ]]; then
local current_name current_email
current_name=$(git config user.name 2>/dev/null || echo "")
current_email=$(git config user.email 2>/dev/null || echo "")
local existing_sig
existing_sig=$(grep "$current_name.*<$current_email>" "$sig_file" || true)
if [[ -n "$existing_sig" ]]; then
verbose_log "Found existing signature in .dco-signatures, restoring marker"
local restored_agreement_commit restored_agreement_change_date restored_signed_date
restored_agreement_commit=$(echo "$existing_sig" | sed -n 's/.*| agreement: \([a-f0-9]*\).*/\1/p')
restored_agreement_change_date=$(echo "$existing_sig" | sed -n 's/.*| agreement: [a-f0-9]* (\(.*\))/\1/p')
restored_signed_date=$(echo "$existing_sig" | sed -n 's/.*| signed: \([^|]*\) |.*/\1/p')
cat > "$marker_file" <<EOF
name=$current_name
email=$current_email
date=$restored_signed_date
agreement_commit=$restored_agreement_commit
agreement_change_date=$restored_agreement_change_date
EOF
echo -e "${GREEN}✓ DCO marker restored from existing signature${NC}"
return 0
fi
fi
# Check if DCO.md exists on disk
if [[ ! -f "$dco_file" ]]; then
echo -e "${RED}Error: DCO.md not found in repository root${NC}" >&2
echo -e "${RED}Cannot proceed without DCO file${NC}" >&2
exit 1
fi
# Check if DCO.md is committed to git; if not, commit it automatically
local has_dco_commits="false"
# git log fails on repos with no commits at all, so handle that
if git rev-parse HEAD >/dev/null 2>&1; then
# Repo has at least one commit - check if DCO.md is tracked
if [[ -n "$(git log --oneline -1 -- "$dco_file" 2>/dev/null)" ]]; then
has_dco_commits="true"
fi
fi
verbose_log "DCO.md has commits: $has_dco_commits"
if [[ "$has_dco_commits" == "false" ]]; then
echo -e "${YELLOW}DCO.md is not yet committed to git. Committing it now...${NC}"
# Save currently staged files (excluding DCO.md)
local staged_files
staged_files=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null | grep -v "^DCO\.md$" || true)
verbose_log "Staged files to preserve: $(echo "$staged_files" | tr '\n' ' ')"
# Unstage everything (may fail on empty repos, that's ok)
if git rev-parse HEAD >/dev/null 2>&1; then
git reset >/dev/null 2>&1 || true
else
# Empty repo: unstage individually
verbose_log "Empty repo detected, unstaging files individually"
if [[ -n "$staged_files" ]]; then
echo "$staged_files" | while IFS= read -r file; do
git rm --cached "$file" >/dev/null 2>&1 || true
done
fi
fi
# Stage and commit only DCO.md
git add "$dco_file"
verbose_log "Staging DCO.md for commit"
local sign_args=()
if [[ -n "$SIGNING_KEY" ]]; then
sign_args+=(--gpg-sign)
fi
if git_commit_with_sign --signoff --no-verify "${sign_args[@]}" -m "[DCO] Set DCO.md Policy by $(git config user.name)" -m "Developer Certificates of Origin established using https://github.com/Stream44/dco" >/dev/null 2>&1; then
echo -e "${GREEN}✓ DCO.md committed to repository${NC}"
else
echo -e "${RED}Error: Failed to commit DCO.md${NC}" >&2
exit 1
fi
# Restore originally staged files
if [[ -n "$staged_files" ]]; then
verbose_log "Restoring staged files"
echo "$staged_files" | while IFS= read -r file; do
git add "$file" 2>/dev/null || true
done
fi
fi
# Display the DCO
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD}${CYAN} DEVELOPER CERTIFICATE OF ORIGIN (DCO)${NC} - tools version ${CYAN}${DCO_VERSION}${NC}"
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo
echo -e "${BLUE}This is your first commit to this repository.${NC}"
echo
# Show repo origin URL
local repo_origin
repo_origin=$(git remote get-url origin 2>/dev/null || echo "")
if [[ -n "$repo_origin" ]]; then
echo -e " ${BOLD}Repository:${NC} ${CYAN}$repo_origin${NC}"
else
echo -e " ${BOLD}Repository:${NC} \033[0;90munknown\033[0m"
fi
# Show .repo-identifier if it exists
local repo_id_file="$git_root/.repo-identifier"
local repo_id
if [[ -f "$repo_id_file" ]]; then
repo_id=$(cat "$repo_id_file" 2>/dev/null | tr -d '[:space:]')
fi
if [[ -n "$repo_id" ]]; then
echo -e " ${BOLD}Identifier:${NC} ${CYAN}$repo_id${NC}"
else
echo -e " ${BOLD}Identifier:${NC} \033[0;90munknown\033[0m"
fi
echo
echo -e "${BLUE}Please read and agree to the Developer Certificate of Origin below.${NC}"
echo
# Ask if ready to review (unless auto-agreeing)
if [[ "$auto_agree" != "true" ]]; then
while true; do
echo -e -n "${YELLOW}Are you ready to review the DCO? (yes/no): ${NC}"
read -r ready_response
case "$ready_response" in
[Yy]es|[Yy])
echo
break
;;
[Nn]o|[Nn])
echo -e "${RED}✗ DCO review required to commit${NC}" >&2
exit 1
;;
*)
echo -e "${RED}Please answer 'yes' or 'no'${NC}"
;;
esac
done
else
echo -e "${GREEN}Auto-agreeing to DCO (--yes-signoff)${NC}"
echo
fi
cat "$dco_file"
echo
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo
echo -e "${MAGENTA}NOTE: You will only be asked to agree once and all future commits will be signed off automatically.${NC}"
echo
# Show who is signing
local git_name git_email
git_name=$(git config user.name 2>/dev/null || echo "")
git_email=$(git config user.email 2>/dev/null || echo "")
if [[ -n "$git_name" ]] && [[ -n "$git_email" ]]; then
echo -e "${BLUE}You are signing as:${NC}"
echo -e " ${BOLD}$git_name <$git_email>${NC}"
if [[ -n "$SIGNING_KEY" ]]; then
local key_fingerprint
key_fingerprint=$(ssh-keygen -lf "$SIGNING_KEY" 2>/dev/null | awk '{print $2}')
echo -e " ${BOLD}Signing key:${NC} ${CYAN}${key_fingerprint}${NC}"
echo -e " ${BOLD}Key path:${NC} ${CYAN}${SIGNING_KEY}${NC}"
fi
echo
fi
# Ask for agreement (unless auto-agreeing)
if [[ "$auto_agree" != "true" ]]; then
while true; do
echo -e -n "${YELLOW}Do you agree to the DCO terms above? (yes/no): ${NC}"
read -r response
case "$response" in
[Yy]es|[Yy])
break
;;
[Nn]o|[Nn])
echo -e "${RED}✗ DCO agreement required to commit${NC}" >&2
exit 1
;;
*)
echo -e "${RED}Please answer 'yes' or 'no'${NC}"
;;
esac
done
fi
# Record the agreement
echo -e "${GREEN}✓ DCO agreement accepted${NC}"
local git_name git_email sign_date agreement_commit agreement_change_date
git_name=$(git config user.name 2>/dev/null || echo "")
git_email=$(git config user.email 2>/dev/null || echo "")
sign_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Get the last commit that changed DCO.md and its date
agreement_commit=$(git log -1 --format='%H' -- "$dco_file")
agreement_change_date=$(git log -1 --format='%ai' -- "$dco_file")
cat > "$marker_file" <<EOF
name=$git_name
email=$git_email
date=$sign_date
agreement_commit=$agreement_commit
agreement_change_date=$agreement_change_date
EOF
# Record the DCO signature in the git tree for permanent record
record_dco_signature "$git_root" "$git_name" "$git_email" "$sign_date" "$agreement_commit" "$agreement_change_date"
echo
return 0
}
# Main function
main() {
local git_root
git_root=$(find_git_root)
# Parse flags; collect remaining args for git commit
local auto_agree="false"
local git_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--yes-signoff)
auto_agree="true"
;;
--verbose)
VERBOSE=true
;;
--signing-key)
shift
SIGNING_KEY="$1"
;;
*)
git_args+=("$1")
;;
esac
shift
done
verbose_log "git_root: $git_root"
verbose_log "auto_agree: $auto_agree"
verbose_log "git_args: ${git_args[*]}"
# Sign the DCO (sign-only, does not commit user code)
show_dco_first_time "$git_root" "$auto_agree"
# If git arguments were provided, run git commit with --signoff
if [[ ${#git_args[@]} -gt 0 ]]; then
verbose_log "Running git commit with signoff and args: ${git_args[*]}"
local config_args=()
local commit_args=()
if [[ -n "$SIGNING_KEY" ]]; then
config_args+=(-c 'gpg.format=ssh' -c "user.signingkey=$SIGNING_KEY")
commit_args+=(--gpg-sign)
fi
git "${config_args[@]}" commit --signoff "${commit_args[@]}" "${git_args[@]}"
fi
}
# Run main function
main "$@"