Bug #173284 - Do not traverse hidden directories such as .svn or .git when
[portage.git] / bin / etc-update
1 #!/bin/bash
2 # Copyright 1999-2007 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6 # Author Brandon Low <lostlogic@gentoo.org>
7 #
8 # Previous version (from which I've borrowed a few bits) by:
9 # Jochem Kossen <j.kossen@home.nl>
10 # Leo Lipelis <aeoo@gentoo.org>
11 # Karl Trygve Kalleberg <karltk@gentoo.org>
12
13 cd /
14
15 if type -P gsed >/dev/null ; then
16         sed() { gsed "$@"; }
17 fi
18
19 get_config() {
20         # the sed here does:
21         #  - strip off comments
22         #  - match lines that set item in question
23         #    - delete the "item =" part
24         #    - store the actual value into the hold space
25         #  - on the last line, restore the hold space and print it
26         # If there's more than one of the same configuration item, then
27         # the store to the hold space clobbers previous value so the last
28         # setting takes precedence.
29         local item=$1
30         eval echo $(sed -n \
31                 -e 's:[[:space:]]*#.*$::' \
32                 -e "/^[[:space:]]*$item[[:space:]]*=/{s:[^=]*=[[:space:]]*\([\"']\{0,1\}\)\(.*\)\1:\2:;h}" \
33                 -e '${g;p}' \
34                 "${PORTAGE_CONFIGROOT}"etc/etc-update.conf)
35 }
36
37 diff_command() {
38         local cmd=${diff_command//%file1/$1}
39         ${cmd//%file2/$2}
40 }
41
42 scan() {
43         echo "Scanning Configuration files..."
44         rm -rf ${TMP}/files > /dev/null 2>&1
45         mkdir ${TMP}/files || die "Failed mkdir command!" 1
46         count=0
47         input=0
48         local find_opts
49         local my_basename
50
51         for path in ${CONFIG_PROTECT} ; do
52                 path="${ROOT}${path}"
53                 # Do not traverse hidden directories such as .svn or .git.
54                 find_opts="-name .* -type d -prune -o -name ._cfg????_*"
55                 if [ ! -d "${path}" ]; then
56                         [ ! -f "${path}" ] && continue
57                         my_basename="${path##*/}"
58                         path="${path%/*}"
59                         find_opts="-maxdepth 1 -name ._cfg????_${my_basename}"
60                 fi
61
62                 ofile=""
63                 # The below set -f turns off file name globbing in the ${find_opts} expansion.
64                 for file in $(set -f ; find ${path}/ ${find_opts} \
65                        ! -name '.*~' ! -iname '.*.bak' -print |
66                            sed -e "s:\(^.*/\)\(\._cfg[0-9]*_\)\(.*$\):\1\2\3\%\2\%\3:" |
67                            sort -t'%' -k3 -k2 | LANG=POSIX LC_ALL=POSIX cut -f1 -d'%'); do
68
69                         rpath=$(echo "${file/\/\///}" | sed -e "s:/[^/]*$::")
70                         rfile=$(echo "${file/\/\///}" | sed -e "s:^.*/::")
71                         for mpath in ${CONFIG_PROTECT_MASK}; do
72                                 mpath="${ROOT}${mpath}"
73                                 mpath=$(echo "${mpath/\/\///}")
74                                 if [[ "${rpath}" == "${mpath}"* ]]; then
75                                         mv ${rpath}/${rfile} ${rpath}/${rfile:10}
76                                         break
77                                 fi
78                         done
79                         if [[ ! -f ${file} ]] ; then
80                                 echo "Skipping non-file ${file} ..."
81                                 continue
82                         fi
83
84                         if [[ "${ofile:10}" != "${rfile:10}" ]] ||
85                            [[ ${opath} != ${rpath} ]]; then
86                                 MATCHES=0
87                                 if [[ "${EU_AUTOMERGE}" == "yes" ]]; then
88                                         if [ ! -e "${rpath}/${rfile}" ] || [ ! -e "${rpath}/${rfile:10}" ]; then
89                                                 MATCHES=0
90                                         else
91                                                 diff -Bbua ${rpath}/${rfile} ${rpath}/${rfile:10} | egrep '^[+-]' | egrep -v '^[+-][\t ]*#|^--- |^\+\+\+ ' | egrep -qv '^[-+][\t ]*$'
92                                                 MATCHES=$?
93                                         fi
94                                 elif [[ -z $(diff -Nua ${rpath}/${rfile} ${rpath}/${rfile:10}|
95                                                           grep "^[+-][^+-]"|grep -v '# .Header:.*') ]]; then
96                                         MATCHES=1
97                                 fi
98                                 if [[ "${MATCHES}" == "1" ]]; then
99                                         echo "Automerging trivial changes in: ${rpath}/${rfile:10}"
100                                         mv ${rpath}/${rfile} ${rpath}/${rfile:10}
101                                         continue
102                                 else
103                                         count=${count}+1
104                                         echo "${rpath}/${rfile:10}" > ${TMP}/files/${count}
105                                         echo "${rpath}/${rfile}" >> ${TMP}/files/${count}
106                                         ofile="${rfile}"
107                                         opath="${rpath}"
108                                         continue
109                                 fi
110                         fi
111
112                         if [[ -z $(diff -Nua ${rpath}/${rfile} ${rpath}/${ofile}|
113                                           grep "^[+-][^+-]"|grep -v '# .Header:.*') ]]; then
114                                 mv ${rpath}/${rfile} ${rpath}/${ofile}
115                                 continue
116                         else
117                                 echo "${rpath}/${rfile}" >> ${TMP}/files/${count}
118                                 ofile="${rfile}"
119                                 opath="${rpath}"
120                         fi
121                 done
122         done
123
124 }
125
126 sel_file() {
127         local -i isfirst=0
128         until [[ -f ${TMP}/files/${input} ]] || \
129               [[ ${input} == -1 ]] || \
130               [[ ${input} == -3 ]]
131         do
132                 local numfiles=$(ls ${TMP}/files|wc -l)
133                 local numwidth=${#numfiles}
134                 for file in $(ls ${TMP}/files|sort -n); do
135                         if [[ ${isfirst} == 0 ]] ; then
136                                 isfirst=${file}
137                         fi
138                         numshow=$(printf "%${numwidth}i${PAR} " ${file})
139                         numupdates=$(( $(wc -l <${TMP}/files/${file}) - 1 ))
140                         echo -n "${numshow}"
141                         if [[ ${mode} == 0 ]] ; then
142                                 echo "$(head -n1 ${TMP}/files/${file}) (${numupdates})"
143                         else
144                                 head -n1 ${TMP}/files/${file}
145                         fi
146                 done > ${TMP}/menuitems
147
148                 if [ "${OVERWRITE_ALL}" == "yes" ]; then
149                         input=0
150                 elif [ "${DELETE_ALL}" == "yes" ]; then
151                         input=0
152                 else
153                         if [[ ${mode} == 0 ]] ; then
154                                 echo "The following is the list of files which need updating, each
155 configuration file is followed by a list of possible replacement files."
156                         else
157                                 local my_title="Please select a file to update"
158                         fi
159
160                         if [[ ${mode} == 0 ]] ; then
161                                 cat ${TMP}/menuitems
162                                 echo    "Please select a file to edit by entering the corresponding number."
163                                 echo    "              (don't use -3, -5, -7 or -9 if you're unsure what to do)"
164                                 echo    "              (-1 to exit) (-3 to auto merge all remaining files)"
165                                 echo    "                           (-5 to auto-merge AND not use 'mv -i')"
166                                 echo    "                           (-7 to discard all updates)"
167                                 echo -n "                           (-9 to discard all updates AND not use 'rm -i'): "
168                                 input=$(read_int)
169                         else
170                                 dialog --title "${title}" --menu "${my_title}" \
171                                         0 0 0 $(echo -e "-1 Exit\n$(<${TMP}/menuitems)") \
172                                         2> ${TMP}/input || die "User termination!" 0
173                                 input=$(<${TMP}/input)
174                         fi
175                         if [[ ${input} == -9 ]]; then
176                                 read -p "Are you sure that you want to delete all updates (type YES):" reply
177                                 if [[ ${reply} != "YES" ]]; then
178                                         continue
179                                 else
180                                         input=-7
181                                         export rm_opts=""
182                                 fi
183                         fi
184                         if [[ ${input} == -7 ]]; then
185                                 input=0
186                                 export DELETE_ALL="yes"
187                         fi
188                         if [[ ${input} == -5 ]] ; then
189                                 input=-3
190                                 export mv_opts=" ${mv_opts} "
191                                 mv_opts="${mv_opts// -i / }"
192                         fi
193                         if [[ ${input} == -3 ]] ; then
194                                 input=0
195                                 export OVERWRITE_ALL="yes"
196                         fi
197                 fi # -3 automerge
198                 if [[ -z ${input} ]] || [[ ${input} == 0 ]] ; then
199                         input=${isfirst}
200                 fi
201         done
202 }
203
204 user_special() {
205         if [ -r ${PORTAGE_CONFIGROOT}etc/etc-update.special ]; then
206                 if [ -z "$1" ]; then
207                         echo "ERROR: user_special() called without arguments"
208                         return 1
209                 fi
210                 while read pat; do
211                         echo ${1} | grep "${pat}" > /dev/null && return 0
212                 done < ${PORTAGE_CONFIGROOT}etc/etc-update.special
213         fi
214         return 1
215 }
216
217 read_int() {
218         # Read an integer from stdin.  Continously loops until a valid integer is
219         # read.  This is a workaround for odd behavior of bash when an attempt is
220         # made to store a value such as "1y" into an integer-only variable.
221         local my_input
222         while true; do
223                 read my_input
224                 # failed integer conversions will break a loop unless they're enclosed
225                 # in a subshell.
226                 echo "${my_input}" | ( declare -i x; read x) && break
227         done
228         echo ${my_input}
229 }
230
231 do_file() {
232         interactive_echo() { [ "${OVERWRITE_ALL}" != "yes" ] && [ "${DELETE_ALL}" != "yes" ] && echo; }
233         interactive_echo
234         local -i my_input
235         local -i fcount=0
236         until (( $(wc -l < ${TMP}/files/${input}) < 2 )); do
237                 my_input=0
238                 if (( $(wc -l < ${TMP}/files/${input}) == 2 )); then
239                         my_input=1
240                 fi
241                 until (( ${my_input} > 0 )) && (( ${my_input} < $(wc -l < ${TMP}/files/${input}) )); do
242                         fcount=0
243
244                         if [ "${OVERWRITE_ALL}" == "yes" ]; then
245                                 my_input=0
246                         elif [ "${DELETE_ALL}" == "yes" ]; then
247                                 my_input=0
248                         else
249                                 for line in $(<${TMP}/files/${input}); do
250                                         if (( ${fcount} > 0 )); then
251                                                 echo -n "${fcount}${PAR} "
252                                                 echo "${line}"
253                                         else
254                                                 if [[ ${mode} == 0 ]] ; then
255                                                         echo "Below are the new config files for ${line}:"
256                                                 else
257                                                         local my_title="Please select a file to process for ${line}"
258                                                 fi
259                                         fi
260                                         fcount=${fcount}+1
261                                 done > ${TMP}/menuitems
262
263                                 if [[ ${mode} == 0 ]] ; then
264                                         cat ${TMP}/menuitems
265                                         echo -n "Please select a file to process (-1 to exit this file): "
266                                         my_input=$(read_int)
267                                 else
268                                         dialog --title "${title}" --menu "${my_title}" \
269                                                 0 0 0 $(echo -e "$(<${TMP}/menuitems)\n${fcount} Exit") \
270                                                 2> ${TMP}/input || die "User termination!" 0
271                                         my_input=$(<${TMP}/input)
272                                 fi
273                         fi # OVERWRITE_ALL
274
275                         if [[ ${my_input} == 0 ]] ; then
276                                 my_input=1
277                         elif [[ ${my_input} == -1 ]] ; then
278                                 input=0
279                                 return
280                         elif [[ ${my_input} == ${fcount} ]] ; then
281                                 break
282                         fi
283                 done
284                 if [[ ${my_input} == ${fcount} ]] ; then
285                         break
286                 fi
287
288                 fcount=${my_input}+1
289
290                 file=$(sed -e "${fcount}p;d" ${TMP}/files/${input})
291                 ofile=$(head -n1 ${TMP}/files/${input})
292
293                 do_cfg "${file}" "${ofile}"
294
295                 sed -e "${fcount}!p;d" ${TMP}/files/${input} > ${TMP}/files/sed
296                 mv ${TMP}/files/sed ${TMP}/files/${input}
297
298                 if [[ ${my_input} == -1 ]] ; then
299                         break
300                 fi
301         done
302         interactive_echo
303         rm ${TMP}/files/${input}
304         count=${count}-1
305 }
306
307 do_cfg() {
308
309         local file="${1}"
310         local ofile="${2}"
311         local -i my_input=0
312
313         until (( ${my_input} == -1 )) || [ ! -f ${file} ]; do
314                 if [[ "${OVERWRITE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
315                         my_input=1
316                 elif [[ "${DELETE_ALL}" == "yes" ]] && ! user_special "${ofile}"; then
317                         my_input=2
318                 else
319                         if [ "${using_editor}" == 0 ]; then
320                                 (
321                                         echo "Showing differences between ${ofile} and ${file}"
322                                         diff_command "${ofile}" "${file}"
323                                 ) | ${pager}
324                         else
325                                 echo "Beginning of differences between ${ofile} and ${file}"
326                                 diff_command "${ofile}" "${file}"
327                                 echo "End of differences between ${ofile} and ${file}"
328                         fi
329                         if [ -L "${file}" ]; then
330                                 echo
331                                 echo "-------------------------------------------------------------"
332                                 echo "NOTE: File is a symlink to another file. REPLACE recommended."
333                                 echo "      The original file may simply have moved. Please review."
334                                 echo "-------------------------------------------------------------"
335                                 echo
336                         fi
337                         echo -n "File: ${file}
338 1) Replace original with update
339 2) Delete update, keeping original as is
340 3) Interactively merge original with update
341 4) Show differences again
342 Please select from the menu above (-1 to ignore this update): "
343                         my_input=$(read_int)
344                 fi
345
346                 case ${my_input} in
347                         1) echo "Replacing ${ofile} with ${file}"
348                            mv ${mv_opts} ${file} ${ofile}
349                            [ -n "${OVERWRITE_ALL}" ] && my_input=-1
350                            continue
351                            ;;
352                         2) echo "Deleting ${file}"
353                            rm ${rm_opts} ${file}
354                            [ -n "${DELETE_ALL}" ] && my_input=-1
355                            continue
356                            ;;
357                         3) do_merge "${file}" "${ofile}"
358                            my_input=${?}
359 #                          [ ${my_input} == 255 ] && my_input=-1
360                            continue
361                            ;;
362                         4) continue
363                            ;;
364                         *) continue
365                            ;;
366                 esac
367         done
368 }
369
370 do_merge() {
371         # make sure we keep the merged file in the secure tempdir
372         # so we dont leak any information contained in said file
373         # (think of case where the file has 0600 perms; during the
374         # merging process, the temp file gets umask perms!)
375
376         local file="${1}"
377         local ofile="${2}"
378         local mfile="${TMP}/${2}.merged"
379         local -i my_input=0
380         echo "${file} ${ofile} ${mfile}"
381
382         if [[ -e ${mfile} ]] ; then
383                 echo "A previous version of the merged file exists, cleaning..."
384                 rm ${rm_opts} "${mfile}"
385         fi
386
387         # since mfile will be like $TMP/path/to/original-file.merged, we
388         # need to make sure the full /path/to/ exists ahead of time
389         mkdir -p "${mfile%/*}"
390
391         until (( ${my_input} == -1 )); do
392                 echo "Merging ${file} and ${ofile}"
393                 $(echo "${merge_command}" |
394                  sed -e "s:%merged:${mfile}:g" \
395                          -e "s:%orig:${ofile}:g" \
396                          -e "s:%new:${file}:g")
397                 until (( ${my_input} == -1 )); do
398                         echo -n "1) Replace ${ofile} with merged file
399 2) Show differences between merged file and original
400 3) Remerge original with update
401 4) Edit merged file
402 5) Return to the previous menu
403 Please select from the menu above (-1 to exit, losing this merge): "
404                         my_input=$(read_int)
405                         case ${my_input} in
406                                 1) echo "Replacing ${ofile} with ${mfile}"
407                                    if  [[ ${USERLAND} == BSD ]] ; then
408                                        chown "$(stat -f %Su:%Sg "${ofile}")" "${mfile}"
409                                        chmod $(stat -f %Mp%Lp "${ofile}") "${mfile}"
410                                    else
411                                        chown --reference="${ofile}" "${mfile}"
412                                        chmod --reference="${ofile}" "${mfile}"
413                                    fi
414                                    mv ${mv_opts} "${mfile}" "${ofile}"
415                                    rm ${rm_opts} "${file}"
416                                    return 255
417                                    ;;
418                                 2)      if [ "${using_editor}" == 0 ]; then
419                                                 (
420                                                         echo "Showing differences between ${ofile} and ${mfile}"
421                                                         diff_command "${ofile}" "${mfile}"
422                                                 ) | ${pager}
423                                         else
424                                                 echo "Beginning of differences between ${ofile} and ${mfile}"
425                                                 diff_command "${ofile}" "${mfile}"
426                                                 echo "End of differences between ${ofile} and ${mfile}"
427                                         fi
428                                    continue
429                                    ;;
430                                 3) break
431                                    ;;
432                                 4) ${EDITOR:-nano -w} "${mfile}"
433                                    continue
434                                          ;;
435                                 5) rm ${rm_opts} "${mfile}"
436                                    return 0
437                                    ;;
438                                 *) continue
439                                    ;;
440                         esac
441                 done
442         done
443         rm ${rm_opts} "${mfile}"
444         return 255
445 }
446
447 die() {
448         trap "" TERM
449         trap "" KILL
450
451         if [ ${2} -eq 0 ]; then
452                 echo "Exiting: ${1}"
453                 scan > /dev/null
454                 [ ${count} -gt 0 ] && echo "NOTE: ${count} updates remaining"
455         else
456                 echo "ERROR: ${1}"
457         fi
458
459         rm -rf "${TMP}"
460         exit ${2}
461 }
462
463 usage() {
464         cat <<-EOF
465         etc-update: Handle configuration file updates
466
467         Usage: etc-update [options]
468
469         Options:
470           -d, --debug    Enable shell debugging
471           -h, --help     Show help and run away
472           -V, --version  Show version and trundle away
473         EOF
474
475         [[ -n ${*:2} ]] && printf "\nError: %s\n" "${*:2}" 1>&2
476
477         exit ${1:-0}
478 }
479
480 #
481 # Run the script
482 #
483
484 SET_X=false
485 while [[ -n $1 ]] ; do
486         case $1 in
487                 -d|--debug)   SET_X=true;;
488                 -h|--help)    usage;;
489                 -V|--version) echo '$Id$'; exit 0;;
490                 *)            usage 1 "Invalid option '$1'";;
491         esac
492         shift
493 done
494 ${SET_X} && set -x
495
496 type portageq > /dev/null || exit $?
497 eval $(portageq envvar -v CONFIG_PROTECT \
498         CONFIG_PROTECT_MASK PORTAGE_CONFIGROOT PORTAGE_TMPDIR ROOT USERLAND)
499 export PORTAGE_TMPDIR
500
501 TMP="${PORTAGE_TMPDIR}/etc-update-$$"
502 trap die term
503
504 [ -w ${PORTAGE_CONFIGROOT}etc ] || die "Need write access to ${PORTAGE_CONFIGROOT}etc" 1
505 #echo $PORTAGE_TMPDIR
506 #echo $CONFIG_PROTECT
507 #echo $CONFIG_PROTECT_MASK
508 #export PORTAGE_TMPDIR=$(/usr/lib/portage/bin/portageq envvar PORTAGE_TMPDIR)
509
510 rm -rf "${TMP}" 2> /dev/null
511 mkdir "${TMP}" || die "failed to create temp dir" 1
512 # make sure we have a secure directory to work in
513 chmod 0700 "${TMP}" || die "failed to set perms on temp dir" 1
514 chown ${UID:-0}:${GID:-0} "${TMP}" || die "failed to set ownership on temp dir" 1
515
516 # I need the CONFIG_PROTECT value
517 #CONFIG_PROTECT=$(/usr/lib/portage/bin/portageq envvar CONFIG_PROTECT)
518 #CONFIG_PROTECT_MASK=$(/usr/lib/portage/bin/portageq envvar CONFIG_PROTECT_MASK)
519
520 # load etc-config's configuration
521 EU_AUTOMERGE=$(get_config eu_automerge)
522 rm_opts=$(get_config rm_opts)
523 mv_opts=$(get_config mv_opts)
524 cp_opts=$(get_config cp_opts)
525 pager=$(get_config pager)
526 diff_command=$(get_config diff_command)
527 using_editor=$(get_config using_editor)
528 merge_command=$(get_config merge_command)
529 declare -i mode=$(get_config mode)
530 [[ -z ${mode} ]] && mode=0
531 [[ -z ${pager} ]] && pager="cat"
532
533 if [ "${using_editor}" == 0 ]; then
534         # Sanity check to make sure diff exists and works
535         echo > "${TMP}"/.diff-test-1
536         echo > "${TMP}"/.diff-test-2
537         
538         if ! diff_command "${TMP}"/.diff-test-1 "${TMP}"/.diff-test-2 ; then
539                 die "'${diff_command}' does not seem to work, aborting" 1
540         fi
541 else
542         if ! type ${diff_command%% *} >/dev/null; then
543                 die "'${diff_command}' does not seem to work, aborting" 1
544         fi
545 fi
546
547 if [[ ${mode} == "1" ]] ; then
548         if ! type dialog >/dev/null || ! dialog --help >/dev/null ; then
549                 die "mode=1 and 'dialog' not found or not executable, aborting" 1
550         fi
551 fi
552
553 #echo "rm_opts: $rm_opts, mv_opts: $mv_opts, cp_opts: $cp_opts"
554 #echo "pager: $pager, diff_command: $diff_command, merge_command: $merge_command"
555
556 if (( ${mode} == 0 )); then
557         PAR=")"
558 else
559         PAR=""
560 fi
561
562 declare -i count=0
563 declare input=0
564 declare title="Gentoo's etc-update tool!"
565
566 scan
567
568 until (( ${input} == -1 )); do
569         if (( ${count} == 0 )); then
570                 die "Nothing left to do; exiting. :)" 0
571         fi
572         sel_file
573         if (( ${input} != -1 )); then
574                 do_file
575         fi
576 done
577
578 die "User termination!" 0