archives2git: [fix] support whitespace and glob chars in option supplied args
[archives2git.git] / archives2git
blob3a825fbc62478950ffee10f959c5ed76afb02efa
1 #!/bin/sh
2 # Copyright © 2013,2014,2017 Géraud Meyer <graud@gmx.com>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 2 as
6 # published by the Free Software Foundation.
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11 # for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program. If not, see <http://www.gnu.org/licenses/>.
16 PROGRAM_NAME="archives2git"
17 PROGRAM_VERSION="0.2+"
18 PROGRAM_USAGE="$PROGRAM_NAME [<options>] [--] <archives_and_directories>"
19 help_message () {
20 cat <<HelpMessage
21 NAME
22 $PROGRAM_NAME - generate a Git history from a series of release tarballs
23 USAGE
24 $PROGRAM_USAGE
25 DESCRIPTION
26 $PROGRAM_NAME replaces the contents of a Git repository subdir with that of
27 an <archive> file (or of another <directory>) and commits the changes.
28 OPTIONS
29 The options are parsed by git-rev-parse(1) with the option --parseopt.
30 $( helpm_options )
31 SHELL VARIABLES
32 The following shell variables are usable if appropriate in the shell code
33 snippets passed as command line options: \$arch, \$archdir, \$archname,
34 \$file, \$date, \$author, \$title, \$tagname.
35 SHELL FUNCTIONS
36 The following shell functions are defined in the script and are usable in
37 the shell code snippets: get_version(filename).
38 ENVIRONMENT
39 ARCHIVES2GIT_ENV
40 After parameter and command substitutions and arithmetic expansion, it
41 gives the path of the config file to source. If unset the default
42 configuration file is sourced instead.
43 GIT_AUTHOR_NAME
44 GIT_AUTHOR_EMAIL
45 GIT_COMMITTER_NAME
46 GIT_COMMITTER_EMAIL
47 FILES
48 ~/.archives2gitrc
49 This file is sourced by the script. An example file is distributed.
50 .git/archives2gitrc
51 This repository configuration file is preferred over the global one
52 if it exists.
53 EXAMPLES
54 Some possible options:
55 $ ARCHIVES2GIT_ENV=/dev/null \\
56 archives2git \$(ls ../oldproject-*.tgz | sort -V) \\
57 # keep (only) the .gitignore file
58 --keep-filter 'test x.gitignore = x"\$file"'
59 --keep-filter 'echo "\$file" | grep -q -f \$HOME/filepats'
60 # suppress the version info from the root directory in the archive
61 --rename 'echo "\${file%-*}"'
62 # allow to add ignored files
63 --gitadd-arg -f
64 # use the archive modification time as the author date (with GNU date)
65 --date 'LC_ALL=C date -r "\$arch"'
66 # do not include the path components in the commit title
67 --title 'echo "\${arch##*/}"'
68 # interactively edit each commit message
69 --gitci-arg "--edit"
70 # keep only the files of the project in the root of the tree
71 --gitfilter $'--subdirectory-filter\noldproject'
72 # move the project's files out of their subdir (while keeping other files)
73 --gitfilter $'--tree-filter\nmv oldproject/.* .; mv oldproject/* .'
74 See also the example configuration file.
75 BUGS
76 Trailing whitespaces in filenames are not supported.
77 AUTHOR
78 $PROGRAM_NAME was written by G.raud Meyer.
79 SEE ALSO
80 git-commit-tree(1), git_load_dirs(1), git-rev-parse(1)
81 HelpMessage
83 OPTS_SPEC="\
84 $PROGRAM_USAGE
86 unpack= command to extract the files of the \$arch in \$TMPDIR
87 strip-ext= sh code to strip the filename extension from \$file
88 repo= work tree subdir (current dir by default)
89 rename= sh code to rename the current \$file of the root
90 keep-filter= sh code to decide whether not to remove the \$file of the root
91 gitadd-arg= argument to pass to git-add
92 date= sh code printing the commit author date for the current \$arch
93 author= sh code printing the commit author name for the current \$arch
94 title= sh code printing the commit title (and an optional body start)
95 body= string appended to the title as a new paragraph
96 gitci-arg= argument to pass to git-commit
97 gitfilter= line separated arguments to pass to git-filter
98 tag tag the commits with the default tagname
99 tagname= tag the commits with the name printed by the given sh code
100 command= sh code to eval after having commited and tagged
101 debug enable the verbose printing of what is done
102 keep-tempdir do not delete the temporary extraction dirs
103 h,help print a usage message and exit
104 helpm print the manual and exit
105 man view the manual in the pager and exit
106 version print the version information and exit
109 # helper functions
110 helpm_options () {
111 printf %s "$OPTS_SPEC" | LC_ALL=C awk '/^--$/ { parse=1; next }
112 function opt(spec) {
113 if (sub(/^[a-zA-Z0-9][=*?!]{0,3}(,|$)/, "-&", spec)) sub(/^-.[=*?!]{0,3},/, "& --", spec)
114 else sub(/^/, "--&", spec)
115 return spec
117 parse { print " \t" opt($1); sub(/^[^ \t]*[ \t]*/,""); if ($0) print "\t\t" $0 }'
119 get_version () {
120 _file=$1; shift
121 _base=$( printf "%s\n" "$_file" |
122 LC_ALL=C sed -r -e '$s;(([0-9]{1,}\.)|[-_])[0-9]{1,}([-_.]{0,1}([0-9]{1,}|[ab]|alpha|beta|gamma))*$;;' )
123 _version=${_file#"$_base"}
124 _version=${_version#"-"}; _version=${_version#"."}; _version=${_version#"_"}
125 printf "%s\n" "${_version}"
127 strip_extension () {
128 printf "%s\n" "$1" | LC_ALL=C sed -r \
129 -e '$s;^([^/]*/)*([^/]{1,})/{0,1}$;\2;' \
130 -e '$s;\.([Zz]|lzo|bz|gz|bz2|xz|lzma|7z|lz|rz|lrz)$;;' \
131 -e '$s;\.(tar|t[Zz]|tzo|tbz|tgz|tbz2|txz|t7z|tlz|lha|lzh|7z|alz|arj|zip|rar|jar|war|ace|cab|a|cpio|shar|rpm|deb)$;;' \
132 -e '$s;\.(orig)$;;'
134 NL='
137 # default parameters
138 UNPACK=
139 STRIP_EXT='strip_extension "$arch"'
140 RENAME='' # $file $arch $archname
141 FILTER='false' # $file $arch $archname
142 DATE='' # $arch $archname
143 TITLE='echo "$arch"' # $arch $archname
144 BODY="" # message taken as is
145 ADDARGS=
146 CIARGS=
147 FILTERHEAD= # line separated arguments
148 TAG=
149 TAGDEF='printf version/; get_version "$archname"'
150 COMMAND=
151 KEEP_TEMPDIR=
152 # config (file, environment, command line)
153 if [ -n "${ARCHIVES2GIT_ENV+set}" ]
154 then
155 dquotedexpr=$( printf "%s" "$ARCHIVES2GIT_ENV" | LC_ALL=C sed 's/"/\\"/g' )
156 # double quoting still allows backslash escapes (as well as substitutions
157 # and expansions)
158 configfile=
159 (eval : '${configfile:="'"$dquotedexpr"'"}') &&
160 eval : '${configfile:="'"$dquotedexpr"'"}'
161 # use := instead of = as the substitution for more portability (which is
162 # not that sensible since the + substitution is not implemented either
163 # when = is not)
164 elif
165 gitdir=$(git rev-parse --git-dir) &&
166 [ -f "$gitdir"/archives2gitrc ]
167 then
168 configfile=$gitdir/archives2gitrc
169 else
170 configfile=$HOME/.archives2gitrc
172 [ -r "$configfile" ] && . "$configfile"
173 GIT_WORK_TREE=${GIT_WORK_TREE-.}
174 # may be a subdir of the top level
175 parsing=$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@"); rc=$?
176 eval "$parsing"
177 if [ $rc -ne 0 ]
178 then printf %s "$parsing" | grep '^EOF' >/dev/null && exit 0 || exit $rc
180 while [ $# -gt 0 ]
182 case $1 in
183 --unpack)
184 shift; UNPACK=$1 ;;
185 --strip-ext)
186 shift; STRIP_EXT=$1 ;;
187 --repo)
188 shift; GIT_WORK_TREE=$1 ;;
189 --rename)
190 shift; RENAME=$1 ;;
191 --keep-filter)
192 shift; FILTER=$1 ;;
193 --gitadd-arg)
194 shift; ADDARGS="$ADDARGS$NL$1" ;;
195 --date)
196 shift; DATE=$1 ;;
197 --author)
198 shift; AUTHOR=$1 ;;
199 --title)
200 shift; TITLE=$1 ;;
201 --body)
202 shift; BODY=$1 ;;
203 --gitci-arg)
204 shift; CIARGS="$CIARGS$NL$1" ;;
205 --gitfilter)
206 shift; FILTERHEAD=$1 ;;
207 --tag)
208 TAG=$TAGDEF ;;
209 --tagname)
210 shift; TAG=$1 ;;
211 --command)
212 shift; COMMAND=$1 ;;
213 --debug)
214 set -x ;;
215 --keep-tempdir)
216 KEEP_TEMPDIR=yes ;;
217 --helpm)
218 help_message
219 exit ;;
220 --man)
221 help_message | helpm2pod --man - ARCHIVES2GIT
222 exit ;;
223 --version)
224 echo "$PROGRAM_NAME version $PROGRAM_VERSION"
225 exit ;;
226 --?*)
227 echo >&2 "$PROGRAM_NAME: internal error: invalid option $1"
228 exit 255 ;;
230 shift; break ;;
232 break ;;
233 esac
234 shift
235 done
236 # setup
237 [ -z "$KEEP_TEMPDIR" ] && trap 'rm -rf "$TMPDIR"' EXIT INT QUIT TERM
238 TMPDIR=$(mktemp -d) ||
240 echo >&2 "$PROGRAM_NAME: error: cannot create a temporary directory"
241 exit 253
243 WRKDIR=$(pwd)
244 # git repository check
245 cd "$GIT_WORK_TREE" &&
246 { test -d .git || git rev-parse --git-dir >/dev/null; } ||
248 echo >&2 "$PROGRAM_NAME: error: not in a git repository ($(pwd))"
249 exit 255
251 test -z "$(git status --porcelain)" &&
252 git update-index -q --refresh ||
254 echo >&2 "$PROGRAM_NAME: error: unstaged files or dirty index"
255 exit 254
258 # main
259 set -e
260 OLDIFS=$IFS
261 for arch
263 # put the files in the archive dir
264 cd "$WRKDIR"
265 if [ -d "$arch" ]
266 then
267 cp -R "$arch" "$TMPDIR"
268 elif [ -n "$UNPACK" ]
269 then
270 (eval "$UNPACK")
271 else
272 aunpack -X "$TMPDIR" "$arch"
274 archdir=$TMPDIR
275 archname=$(eval "$STRIP_EXT")
276 # remove (almost) everything from the repository dir
277 cd "$GIT_WORK_TREE"
278 for file in * .*
280 [ -e "$file" ] || [ -h "$file" ] || continue
281 [ x"$file" = x"." ] || [ x"$file" = x".." ] || [ x"$file" = x".git" ] && continue
282 (eval "$FILTER") || { git rm -r "$file" || rm -R "$file"; }
283 done
284 # move the content of the temp dir to the repository and stage it
285 for file in "$archdir"/* "$archdir"/.*
287 [ -e "$file" ] || [ -h "$file" ] || continue
288 file=${file#$archdir/}
289 [ x"$file" = x"." ] || [ x"$file" = x".." ] && continue
290 [ -n "$RENAME" ] && name=$(eval "$RENAME") || name=$file
291 if [ -n "$name" ]
292 then
293 if [ -e ./"$name" ]
294 then
295 # remove conflicting file (previously kept or renamed to the same name)
296 rm -R ./"$name"
297 echo >&2 "WARNING: conflicting file removed: \`$name'"
299 mv "$archdir"/"$file" ./"$name"
300 IFS=$NL; set -f
301 git add $ADDARGS ./"$name"
302 set +f; IFS=$OLDIFS
303 else
304 rm -R "$archdir"/"$file"
306 unset name
307 done
308 unset file
309 # commit, filter and clean up
310 date=$(eval "$DATE")
311 author=$(eval "$AUTHOR")
312 title=$(eval "$TITLE")
313 IFS=$NL; set -f
314 git commit ${AUTHOR:+--author$NL"$author"} -m "$title"${BODY:+"$NL$NL$BODY"} ${DATE:+--date$NL"$date"} $CIARGS
315 if [ -n "$FILTERHEAD" ]
316 then
317 git filter-branch $FILTERHEAD -- HEAD^..HEAD
318 rm -R "$(git rev-parse --git-dir)"/refs/original/
320 set +f; IFS=$OLDIFS
321 if [ -n "$TAG" ]
322 then
323 tagmessage=$(eval "$TAG" | LC_ALL=C tail -n +2)
324 tagname=$(eval "$TAG" | LC_ALL=C head -1)
325 git tag ${tagmessage:+-m "$tagmessage"} "$tagname"
327 if [ -n "$COMMAND" ]
328 then
329 (eval "$COMMAND")
331 unset title date author tagname tagmessage
332 done