repo-commit / repo-commit.in

  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
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
#!/bin/sh
# repo-commit -- a Gentoo repository commit helper
# (c) 2011-2012 Michał Górny & Nathan Phillip Brink
# Released under the terms of the 2-clause BSD license.

# -- output helpers --

# Output the message to STDERR.
say() {
	echo "${@}" >&2
}

# Output the error message and abort the script with non-zero status.
die() {
	say "${RED}${@}${RESET}"
	exit 1
}

# Output the debug message if --verbose was used.
sayv() {
	[ -n "${SC_VERBOSE}" ] && say "${GREEN}${@}${RESET}"
}

# Execute the command and die with simple error message if it fails.
req() {
	"${@}" || die "'${@}' failed."
}

# Append the arguments to an IFS-separated list variable whose name was
# passed as the first arg.
array_append() {
	local varname
	varname=${1}
	shift

	eval "set -- \${${varname}} \"\${@}\"; ${varname}=\${*}"
}

# -- POSIX compat --

# Check whether 'local' is supported.
local_supported() {
	PATH= local test 2>/dev/null
}

# If it is not, declare dummy local() function unsetting the variables.
( local_supported ) || eval 'local() {
	unset "${@}"
}'

# -- 'look around' functions --

# See if we're in a repo, and what VCS are we using.
find_repo() {
	if svn info >/dev/null 2>&1; then
		SC_VCS=svn
		: ${SC_WANT_CHANGELOG=1}
	elif cvs status -l >/dev/null 2>&1; then
		SC_VCS=cvs
		: ${SC_WANT_CHANGELOG=1}
	elif hg tip >/dev/null 2>&1; then
		SC_VCS=hg
	else
		local remotes ret
		remotes=$(git branch -r 2>/dev/null)
		ret=${?}

		if [ ${ret} -ne 127 ] && [ ${ret} -ne 128 ]; then
			if echo "${remotes}" | grep git-svn >/dev/null 2>&1; then
				: ${SC_WANT_CHANGELOG=1}
			fi
			SC_VCS=git
		else
			die 'Unable to find any familiar repository type (are you inside the repo?).'
		fi
	fi

	sayv "Ok, we're in the ${SC_VCS} working tree. Let's see what I can do around here..."
}

# Check whether a particular directory has been completely removed
# from the repo.
is_whole_dir_removed() {
	if [ ${SC_VCS} = svn ]; then
		[ "$(svn status --depth=empty -- "${1}" | wc -l)" = 1 ]
	elif [ ${SC_VCS} = git ]; then
		[ -z "$(git ls-files -c -- "${1}")" ]
	elif [ ${SC_VCS} = hg ]; then
		[ -z "$(hg status -madc "${1}")" ]
	elif [ ${SC_VCS} = cvs ]; then
		[ -z "$(cvs -Q status -R "${1}" 2>/dev/null | grep '^File:' | grep -v 'Status: Locally Removed$')" ]
	fi
}

# Check whether we're having a clean package removal.
is_package_removal() {
	local fields list
	[ -d profiles ] && fields=1-2 || fields=1

	if [ ${SC_VCS} = git ]; then
		list=$(git diff-index --relative --name-only --diff-filter=D HEAD)
	elif [ ${SC_VCS} = hg ]; then
		list=$(hg status -nr .)
	elif [ ${SC_VCS} = cvs ]; then
		list=$(cvs -n -q up 2>/dev/null | sed -n -e 's/^R//p')
	elif [ ${SC_VCS} = svn ]; then
		list=$(svn status -q | sed -n -e 's/^D       //p')
	fi
	list=$(echo "${list}" | cut -d / -f ${fields} | sort | uniq)

	# 1) We have to have any removes.
	[ -z "${list}" ] && return 1

	# Few more checks.
	local dir olist
	for dir in ${list}; do
		# 2) These removes have to remove whole directories.
		is_whole_dir_removed ${dir} && array_append olist "${dir}"
	done

	[ -z "${olist}" ] && return 1

	SC_CHANGE_LIST=${olist}
	return 0
}

# Look around for ebuilds; determine the scenario we're working on.
find_ebuilds() {
	# POSIX is fun -- look for ebuilds in the current directory.
	if [ -n "$(find \( -name '*.ebuild' -print -o ! -name '.' -prune \))" ]; then
		local stripped

		# Get CATEGORY and PN.
		stripped=${PWD%/*}
		stripped=${stripped%/*}
		SC_CP=${PWD#${stripped}/}

		SC_SCENARIO=ebuild-commit
		sayv "We found ebuilds for ${SC_CP} here."
	elif is_package_removal; then
		local cplist category pkg rootprefix
		# We can either have the category on the list or in PWD.
		if [ -d profiles ]; then
			category=
		else
			local stripped
			stripped=${PWD%/*}
			category=${PWD#${stripped}/}/
		fi

		SC_CP=
		SC_REMOVED_PACKAGE_LIST=
		# Now we can have multiple packages around.
		for pkg in ${SC_CHANGE_LIST}; do
			if [ -z "${category}" ]; then
				case ${pkg} in
					eclass/*|licenses/*|local/*|profiles/*|scripts/*)
						continue
						;;
				esac
			fi
			SC_CP=${SC_CP:+${SC_CP}, }${category}${pkg}
			array_append SC_REMOVED_PACKAGE_LIST "${category}${pkg}"
		done

		SC_ROOT=${category:+../}

		# Replace with the filtered version, placing all atoms
		# relative to SC_ROOT.
		SC_CHANGE_LIST=
		for pkg in ${SC_REMOVED_PACKAGE_LIST}; do
			array_append SC_CHANGE_LIST "${SC_ROOT}${pkg}"
		done

		local sdir
		for sdir in eclass licenses profiles; do
			check_for_changes ${SC_ROOT}${sdir} >/dev/null && SC_CHANGE_LIST="${SC_CHANGE_LIST} ${SC_ROOT}${sdir}"
		done
		SC_SCENARIO=package-removal
		sayv "We're removing ${SC_CP}."
	else
		die 'No familiar scenario found.'
	fi
}

# -- VCS helpers --

# Check whether a particular locations have changed, ignoring ChangeLog
# changes.
check_for_changes() {
	local output

	if [ ${SC_VCS} = git ]; then
		output=$(git diff-index --name-only --relative HEAD -- "${@}")
	elif [ ${SC_VCS} = hg ]; then
		output=$(hg status -- ${1-.} "${@}")
	elif [ ${SC_VCS} = svn ]; then
		output=$(svn status -- "${@}")
	elif [ ${SC_VCS} = cvs ]; then
		# `U' indicates a remote, incoming update.
		output=$(cvs -n -q update -R -- "${@}" 2>/dev/null | grep -v '^U')
	fi

	[ -z "${output}" ] && return 1
	# We do not care about user mangling ChangeLog, we will reset it anyway.
	echo "${output}" | grep -v ChangeLog >/dev/null
}

# Discard any changes to a particular set of files.
vcs_reset() {
	if [ ${SC_VCS} = git ]; then
		git checkout HEAD -- "${@}" 2>/dev/null || req rm -f -- "${@}"
	elif [ ${SC_VCS} = hg ]; then
		[ -n "$(hg status -au "${@}")" ] && req rm -f -- "${@}"
		hg revert --no-backup -- "${@}" 2>/dev/null
	elif [ ${SC_VCS} = svn ]; then
		req rm -f -- "${@}"
		svn revert -- "${@}" >/dev/null
	elif [ ${SC_VCS} = cvs ]; then
		# cvs update -C does exist, but it sometimes doesn't
		# work.
		req rm -f -- "${@}"
		cvs update -- "${@}" >/dev/null 2>&1
	fi
}

# Request VCS to provide a verbose status report.
vcs_status() {
	if [ ${SC_VCS} = git ]; then
		git status -s -- ${1-.} "${@}"
	elif [ ${SC_VCS} = hg ]; then
		hg status -- ${1-.}  "${@}"
	elif [ ${SC_VCS} = svn ]; then
		svn status -- "${@}"
	elif [ ${SC_VCS} = cvs ]; then
		cvs -n -q up -- "${@}" 2>/dev/null | grep -v '^U'
	fi
}

# Request VCS to provide a verbose diff.
vcs_diff() {
	if [ ${SC_VCS} = git ]; then
		git --no-pager diff HEAD -- ${1-.}
	elif [ ${SC_VCS} = hg ]; then
		hg diff -- ${1-.}
	elif [ ${SC_VCS} = svn ]; then
		svn diff -- "${@}"
	elif [ ${SC_VCS} = cvs ]; then
		cvs -n -q diff -u -p -- "${@}"
	fi
}

# Add particular files to the repository.
vcs_add() {
	${SC_VCS} add -- "${@}"
}

# Commit the specified objects using the commit message provided
# as the first argument. Does not return.
vcs_commit() {
	local msg
	msg=${1}
	shift

	if [ ${SC_VCS} = git ]; then
		exec git commit -m "${msg}" ${1+-o} -- "${@}"
	elif [ ${SC_VCS} = hg ]; then
		exec hg commit -m "${msg}" -- ${1-.} "${@}"
	elif [ ${SC_VCS} = svn ]; then
		exec svn commit -m "${msg}" -- "${@}"
	elif [ ${SC_VCS} = cvs ]; then
		exec cvs commit -m "${msg}" -- "${@}"
	fi
}

# Call VCS to update the working copy to HEAD revision.
vcs_update() {
	# Unlike svn, DVCSes don't push the changes to their origins immediately.
	# That's why we don't force update to it right here.
	if [ ${SC_VCS} = svn ]; then
		sayv "Updating the repository..."
		svn up -- "${@}" || say "Warning: ${SC_VCS} up failed, trying to proceed anyway."
	elif [ ${SC_VCS} = cvs ]; then
		sayv "Updating the repository..."
		cvs up -d -P -- "${@}" || say "Warning: ${SC_VCS} up failed, trying to proceed anyway."
	fi
}

# Check the spelling of the commit message if enabled
check_spelling() {
	if [ -n "${no_check_spelling}" ]; then
		echo "${@}"
		return
	fi

	local speller misspelled_words
	for speller in "enchant -l -d en | cat" \
		"aspell -l en list | cat" \
		"hunspell -l -d en_US | hunspell -l -d en_GB" \
		"ispell -l -denglish | ispell -l -dbritish"; do
		if echo | ${speller%|*} 2>/dev/null \
				&& echo | ${speller%|*} | ${speller#*|} 2>/dev/null; then
			misspelled_words=$(echo "${@}" | ${speller%|*} | ${speller#*|})
			break
		fi
	done

	local word expressions
	for word in ${misspelled_words}; do
		case ${word} in
			[Ee]build|[Gg]entoo|[Gg]entoo-x86|${SC_CP#*/}*|${SC_CP%/*})
				continue
				;;
			[Rr]epoman|[Mm]etadata|[Xx][Mm][Ll])
				continue
				;;
		esac
		expressions="${expressions} -e s/\\(^\\|[^a-zA-Z]\\)\\(${word}\\)\\([^a-zA-Z]\\|\$\\)/\\1${RED}\\2${RESET}\\3/g"
	done

	# sed can't handle zero expressions.
	if [ -z "${expressions}" ]; then
		echo "${@}"
		return
	fi

	echo "${@}" | sed ${expressions}
}

# Print the help message.
print_help() {
	cat <<_EOH_
Synopsis:
	repo-commit [options] [--] <commit message>

Options:
	-?, -h, --help		print this message,
	-V, --version		print version string,

	-c, --changelog		force creating a ChangeLog entry,
	-C, --nocolor		disable colorful output,
	-d, --noupdate		disable updating the repository,
	--diff			display diff of changes before committing,
	-f, --force		force repoman to proceed with the commit,
	-H, --nochangelog	do not append to ChangeLog nor revert it,
	-m, --noformat		do not prepend the commit message with package names,
	-S, --nospelling	do not try to check commit message's spelling,
	-q, --quiet		backwards compat (ignored),
	-t, --trivial		trivial changes (do not add a ChangeLog entry),
	-v, --verbose		enable verbose output,
	-y, --noask		do not ask before committing (avoid interactivity).
_EOH_
}

# Request confirmation before committing. Abort if it is not granted.
confirm() {
	${SC_NOASK+return}
	while true; do
		local answ
		printf '\n%s' "${WHITE}Commit changes?${RESET} [${BGREEN}Yes${RESET}/${RED}No${RESET}] ${GREEN}" >&2
		read answ
		printf '%s' "${RESET}"

		case "${answ}" in
			[yY]|[yY][eE]|[yY][eE][sS])
				break
				;;
			[nN]|[nN][oO])
				die 'Aborting.'
				;;
			*)
				say "Your response '${answ}' not understood, try again."
		esac
	done
}

# Guess what!
main() {
	local no_check_spelling commitmsg force monochrome noprepend noupdate \
		trivial print_diff repoman_changelog
	unset SC_NOASK SC_VERBOSE SC_WANT_CHANGELOG

	# Command-line parsing.
	while [ ${#} -gt 0 ]; do
		case "${1}" in
			--help|-\?|-h)
				print_help
				exit 0
				;;
			--version|-V)
				echo '@PACKAGE_STRING@'
				exit 0
				;;

			-c|--changelog)
				SC_WANT_CHANGELOG=force
				;;
			-C|--nocolor)
				monochrome=1
				;;
			-d|--noupdate)
				noupdate=1
				;;
			--diff)
				print_diff=1
				;;
			-f|--force)
				force=1
				;;
			-H|--nochangelog)
				SC_WANT_CHANGELOG=
				;;
			-m|--noformat)
				noprepend=
				;;
			-q|--quiet)
				;;
			-S|--nospelling|--no-spelling)
				no_check_spelling=1
				;;
			-t|--trivial)
				trivial=1
				;;
			-v|--verbose)
				SC_VERBOSE=1
				;;
			-y|--noask)
				SC_NOASK=
				;;

			--)
				shift
				array_append commitmsg "${@}"
				break
				;;
			-*)
				die "Unknown option: ${1}; see --help." >&2
				;;
			*)
				array_append commitmsg "${1}"
				;;
		esac
		shift
	done

	# Initialize colors.
	if [ -n "${monochrome}" ]; then
		RESET=
		RED=
		GREEN=
		BGREEN=
		YELLOW=
		WHITE=
	else
		local esc
		esc=$(printf '\033[')

		RESET=${esc}0m
		RED="${esc}1;31m"
		GREEN=${esc}32m
		BGREEN="${esc}1;32m"
		YELLOW="${esc}1;33m"
		WHITE="${esc}1;37m"
	fi

	[ -n "${commitmsg}" ] || die 'No commit message provided.'

	# Look around.
	find_repo
	find_ebuilds

	case ${SC_SCENARIO} in
		# Committing changes within the ebuild directory.
		# This includes committing new ebuilds.
		ebuild-commit)
			check_for_changes || die 'No changes found to commit.'

			if [ -z "${noupdate}" ]; then
				vcs_update
			fi

			local bns bn word bug_next
			for word in ${commitmsg}; do
				case ${word} in
					[Bb][Uu][Gg])
						bug_next=1
						;;
					\#*)
						bn="${word#\#}"
						;;
					*)
						[ -z "${bug_next}" ] && continue
						bn="${word}"
						bug_next=
						;;
				esac
				if [ -n "${bn}" ]; then
					bns="${bns:+${bns} }$(expr "${bn}" : '[^[:digit:]]*\([[:digit:]]\{1,\}\)')"
					bn=
				fi
			done

			# With DVCS repos, we do not do ChangeLogs by default...
			# ...at least unless they're already there.
			[ -f ChangeLog ] && : ${SC_WANT_CHANGELOG=1}
			if [ -n "${SC_WANT_CHANGELOG}" ]; then
				sayv 'Cleaning up the ChangeLog...'
				vcs_reset ChangeLog

				# Creating a new ChangeLog? Let's take a look at the commit message.
				if [ ! -f ChangeLog ]; then
					[ -n "${trivial}" ] && die "Trivial change for an initial commit? You're joking, right?"

					# Sunrise-specific checks.
					if [ "$(cat ../../profiles/repo_name 2>/dev/null)" = "sunrise" ]; then
						[ -z "${bns}" ] && die 'Please supply the bug number in the initial commit message!'
						if [ ! -f metadata.xml ]; then
							req cp ../../skel.metadata.xml metadata.xml
							# Output similar to echangelog.
							[ -n "${print_diff}" ] || diff -dup /dev/null metadata.xml
							req vcs_add metadata.xml
						fi
					fi
				fi

				# create ChangeLog entries using repoman if possible
				repoman --version --echangelog=y >/dev/null 2>&1
				if [ ${?} -ne 2 ]; then
					if [ -z "${trivial}" ]; then
						repoman_changelog='--echangelog=y'
					else
						repoman_changelog='--echangelog=n'
					fi
				else
					if [ -z "${trivial}" ]; then
						local ecopts
						[ ${SC_WANT_CHANGELOG} = force ] && ecopts=--no-strict
						sayv '...and appending to it.'
						echangelog --vcs ${SC_VCS} ${ecopts} -- "${commitmsg}" \
							|| die 'Please correct the problems shown by echangelog.'
						echo
					fi
				fi
			fi

			if [ -n "${bns}" ]; then
				local bn cbn
				for bn in ${bns}; do
					cbn=#${WHITE}${bn}${NORMAL}
					wget -q --no-check-certificate \
						"https://bugs.gentoo.org/show_bug.cgi?id=${bn}" -O - \
					| sed -n \
						-e "s, *<title>\(Gentoo \)\?Bug \([0-9]*\) \(-\|&ndash;\) \(.*\)</title>,Bug ${cbn}: ${BGREEN}\4${RESET},p" \
						-e "s, *<title>\(Gentoo \)\?\(Invalid Bug ID\)</title>,Bug ${cbn}: ${YELLOW}!! \2${RESET},p"
				done
				echo
			fi

			if [ -n "${print_diff}" ]; then
				vcs_diff
			elif [ ${SC_VCS} != cvs ] || [ -n "${noupdate}" ]; then
				vcs_status
			fi
			echo

			# Since commit 32264c3, repoman supports '--ask' option,
			# which requests user confirmation before the commit.
			# We like that, because it does it in the right place.
			#
			# If user is using earlier repoman version, we need to
			# request that confirmation ourselves. As we would like
			# the user to see 'repoman full' results first, we need
			# to call it ourselves. Moreover, it requires Manifest to be
			# up-to-date, so we need to call 'repoman manifest' too.
			#
			# That's pretty sad, because it means we're wasting time
			# calling the same repoman functions twice (once manually,
			# then within 'repoman commit'). That's why we would be
			# happy if user updated his/her Portage, and we'd like to
			# encourage him/her to do so -- but we'll have to delay that
			# until a new Portage version is released.

			local old_repoman
			repoman --version -a >/dev/null 2>&1
			if [ ${?} -eq 2 ]; then
				old_repoman=

				#say "${GREEN}Please consider updating portage to newer version.${RESET}"
				#say
				req repoman manifest
				if ! repoman full; then
					[ -n "${force}" ] || die 'Please correct the problems shown by repoman.'
				fi
			fi

			# In CVS, we don't prepend the package name to the commit message.
			[ ${SC_VCS} = cvs ] && noprepend=

			say "${BGREEN}Ready to commit using the following commit message:${RESET}"
			say "${noprepend-${SC_CP}: }$(check_spelling "${commitmsg}")"
			${old_repoman+confirm}

			sayv "Now, let's let repoman do its job..."
			exec repoman commit ${old_repoman-${SC_NOASK--a}} ${force+-f} ${repoman_changelog} -m "${noprepend-${SC_CP}: }${commitmsg}"
			;;

		# Clean removal of a package set.
		package-removal)
			vcs_status ${SC_CHANGE_LIST}
			echo

			local pkg regex
			regex=
			for pkg in ${SC_REMOVED_PACKAGE_LIST}; do
				regex=${regex:+${regex}\|}${pkg}
			done

			say "${GREEN}Grepping for package references...${RESET}"
			# -n is for line numbers, -C would be non-POSIX
			if grep -n "${regex}" ${SC_ROOT}*/*/*.ebuild ${SC_ROOT}*/*/metadata.xml ${SC_ROOT}profiles/package.mask 2>/dev/null; then
				echo
				[ -n "${force}" ] || die 'Please remove the removed package references or use --force.'
			else
				echo
			fi

			say "${BGREEN}Ready to commit ${WHITE}$(echo ${SC_REMOVED_PACKAGE_LIST} | wc -w)${BGREEN} package removal(s), with commit message:${RESET}"
			say "${SC_CP}: $(check_spelling "${commitmsg}")"
			confirm

			if [ -z "${noupdate}" ]; then
				vcs_update ${SC_CHANGE_LIST}
			fi

			vcs_commit "${noprepend-${SC_CP}: }${commitmsg}" ${SC_CHANGE_LIST}
			;;
	esac
}

main "${@}"
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.