archives2git: [fix] do not interpret quotes in $ARCHIVES2GIT_ENV
[archives2git.git] / archives2git
blob7b5a3abc2b7f3daf2015b838672213cc4c602e28
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 EXAMPLES
51 Some possible options:
52 $ archives2git \$(ls ../oldproject-*.tgz | sort -V) \\
53 # keep (only) the .gitignore file
54 --keep-filter 'test x.gitignore = x"\$file"'
55 --keep-filter 'echo "\$file" | grep -q -f \$HOME/filepats'
56 # suppress the version info from the root directory in the archive
57 --rename 'echo "\${file%-*}"'
58 # allow to add ignored files
59 --gitadd-arg -f
60 # use the archive modification time as the author date (with GNU date)
61 --date 'LC_ALL=C date -r "\$arch"'
62 # do not include the path components in the commit title
63 --title 'echo "\${arch##*/}"'
64 # interactively edit each commit message
65 --gitci-arg "--edit"
66 # keep only the files of the project in the root of the tree
67 --gitfilter $'--subdirectory-filter\noldproject'
68 # move the project's files out of their subdir (while keeping other files)
69 --gitfilter $'--tree-filter\nmv oldproject/.* .; mv oldproject/* .'
70 See also the example configuration file.
71 NOTES
72 The code snippets passed to eval are not guaranteed to run in a subshell,
73 so make sure they do not change the shell environment.
74 BUGS
75 Trailing whitespaces in filenames are not supported.
76 AUTHOR
77 $PROGRAM_NAME was written by G.raud Meyer.
78 SEE ALSO
79 git-commit-tree(1), git_load_dirs(1), git-rev-parse(1)
80 HelpMessage
82 OPTS_SPEC="\
83 $PROGRAM_USAGE
85 unpack= command to extract the files of the \$arch in \$TMPDIR
86 strip-ext= sh code to strip the filename extension from \$file
87 repo= work tree subdir (current dir by default)
88 rename= sh code to rename the current \$file of the root
89 keep-filter= sh code to decide whether not to remove the \$file of the root
90 gitadd-arg= argument to pass to git-add
91 date= sh code printing the commit author date for the current \$arch
92 author= sh code printing the commit author name for the current \$arch
93 title= sh code printing the commit title (and an optional body start)
94 body= string appended to the title as a new paragraph
95 gitci-arg= argument to pass to git-commit
96 gitfilter= line separated arguments to pass to git-filter
97 tag tag the commits with the default tagname
98 tagname= tag the commits with the name printed by the given sh code
99 command= sh code to eval after having commited and tagged
100 debug enable the verbose printing of what is done
101 h,help print a usage message and exit
102 helpm print the manual and exit
103 man view the manual in the pager and exit
104 version print the version information and exit
107 # helper functions
108 helpm_options () {
109 printf %s "$OPTS_SPEC" | LC_ALL=C awk '/^--$/ { parse=1; next }
110 function opt(spec) {
111 if (sub(/^[a-zA-Z0-9][=*?!]{0,3}(,|$)/, "-&", spec)) sub(/^-.[=*?!]{0,3},/, "& --", spec)
112 else sub(/^/, "--&", spec)
113 return spec
115 parse { print " \t" opt($1); sub(/^[^ \t]*[ \t]*/,""); if ($0) print "\t\t" $0 }'
117 get_version () {
118 _file=$1; shift
119 _base=$( printf "%s\n" "$_file" |
120 LC_ALL=C sed -r -e '$s;(([0-9]{1,}\.)|[-_])[0-9]{1,}([-_.]{0,1}([0-9]{1,}|[ab]|alpha|beta|gamma))*$;;' )
121 _version=${_file#"$_base"}
122 _version=${_version#"-"}; _version=${_version#"."}; _version=${_version#"_"}
123 printf "%s\n" "${_version}"
125 strip_extension () {
126 printf "%s\n" "$1" | LC_ALL=C sed -r \
127 -e '$s;^([^/]*/)*([^/]{1,})/{0,1}$;\2;' \
128 -e '$s;\.([Zz]|lzo|bz|gz|bz2|xz|lzma|7z|lz|rz|lrz)$;;' \
129 -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)$;;' \
130 -e '$s;\.(orig)$;;'
133 # default parameters
134 UNPACK=
135 STRIP_EXT='strip_extension "$arch"'
136 RENAME='' # $file $arch $archname
137 FILTER='false' # $file $arch $archname
138 DATE='' # $arch $archname
139 TITLE='echo "$arch"' # $arch $archname
140 BODY="" # message taken as is
141 ADDARGS=
142 CIARGS=
143 FILTERHEAD= # line separated arguments
144 TAG=
145 TAGDEF='printf version/; get_version "$archname"'
146 COMMAND=
147 # config (file, environment, command line)
148 if [ -n "${ARCHIVES2GIT_ENV+set}" ]
149 then
150 dquotedexpr=$( printf "%s" "$ARCHIVES2GIT_ENV" | LC_ALL=C sed 's/"/\\"/g' )
151 # double quoting still allows backslash escapes (as well as substitutions
152 # and expansions)
153 configfile=
154 (eval : '${configfile:="'"$dquotedexpr"'"}') &&
155 eval : '${configfile:="'"$dquotedexpr"'"}'
156 # use := instead of = as the substitution for more portability (which is
157 # not that sensible since the + substitution is not implemented either
158 # when = is not)
159 else
160 configfile=$HOME/.archives2gitrc
162 [ -r "$configfile" ] && . "$configfile"
163 GIT_WORK_TREE=${GIT_WORK_TREE-.}
164 # may be a subdir of the top level
165 parsing=$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@"); rc=$?
166 eval "$parsing"
167 if [ $rc -ne 0 ]
168 then printf %s "$parsing" | grep '^EOF' >/dev/null && exit 0 || exit $rc
170 while [ $# -gt 0 ]
172 case $1 in
173 --unpack)
174 shift; UNPACK=$1 ;;
175 --strip-ext)
176 shift; STRIP_EXT=$1 ;;
177 --repo)
178 shift; GIT_WORK_TREE=$1 ;;
179 --rename)
180 shift; RENAME=$1 ;;
181 --keep-filter)
182 shift; FILTER=$1 ;;
183 --gitadd-arg)
184 shift; ADDARGS="$ADDARGS $1" ;;
185 --date)
186 shift; DATE=$1 ;;
187 --author)
188 shift; AUTHOR=$1 ;;
189 --title)
190 shift; TITLE=$1 ;;
191 --body)
192 shift; BODY=$1 ;;
193 --gitci-arg)
194 shift; CIARGS="$CIARGS $1" ;;
195 --gitfilter)
196 shift; FILTERHEAD=$1 ;;
197 --tag)
198 TAG=$TAGDEF ;;
199 --tagname)
200 shift; TAG=$1 ;;
201 --command)
202 shift; COMMAND=$1 ;;
203 --debug)
204 set -x ;;
205 --helpm)
206 help_message
207 exit ;;
208 --man)
209 help_message | helpm2pod --man - ARCHIVES2GIT
210 exit ;;
211 --version)
212 echo "$PROGRAM_NAME version $PROGRAM_VERSION"
213 exit ;;
214 --?*)
215 echo >&2 "$PROGRAM_NAME: internal error: invalid option $1"
216 exit 255 ;;
218 shift; break ;;
220 break ;;
221 esac
222 shift
223 done
224 # setup
225 NL='
227 trap 'rm -rf "$TMPDIR"' EXIT INT QUIT TERM
228 TMPDIR=$(mktemp -d) ||
230 echo >&2 "$PROGRAM_NAME: error: cannot create a temporary directory"
231 exit 253
233 WRKDIR=$(pwd)
234 # git repository check
235 cd "$GIT_WORK_TREE" &&
236 { test -d .git || git rev-parse --git-dir >/dev/null; } ||
238 echo >&2 "$PROGRAM_NAME: error: not in a git repository ($(pwd))"
239 exit 255
241 test -z "$(git status --porcelain)" &&
242 git update-index -q --refresh ||
244 echo >&2 "$PROGRAM_NAME: error: unstaged files or dirty index"
245 exit 254
248 # main
249 set -e
250 OLDIFS=$IFS
251 for arch
253 # put the files in the archive dir
254 cd "$WRKDIR"
255 if [ -d "$arch" ]
256 then
257 cp -R "$arch" "$TMPDIR"
258 elif [ -n "$UNPACK" ]
259 then
260 eval "$UNPACK"
261 else
262 aunpack -X "$TMPDIR" "$arch"
264 archdir=$TMPDIR
265 archname=$(eval "$STRIP_EXT")
266 # remove (almost) everything from the repository dir
267 cd "$GIT_WORK_TREE"
268 for file in * .*
270 [ -e "$file" ] || [ -h "$file" ] || continue
271 [ x"$file" = x"." ] || [ x"$file" = x".." ] || [ x"$file" = x".git" ] && continue
272 eval "$FILTER" || { git rm -r "$file" || rm -R "$file"; }
273 done
274 # move the content of the temp dir to the repository and stage it
275 for file in "$archdir"/* "$archdir"/.*
277 [ -e "$file" ] || [ -h "$file" ] || continue
278 file=${file#$archdir/}
279 [ x"$file" = x"." ] || [ x"$file" = x".." ] && continue
280 [ -n "$RENAME" ] && name=$(eval "$RENAME") || name=$file
281 if [ -n "$name" ]
282 then
283 if [ -e ./"$name" ]
284 then
285 # remove conflicting file (previously kept or renamed to the same name)
286 rm -R ./"$name"
287 echo >&2 "WARNING: conflicting file removed: \`$name'"
289 mv "$archdir"/"$file" ./"$name"
290 git add $ADDARGS ./"$name"
291 else
292 rm -R "$archdir"/"$file"
294 unset name
295 done
296 unset file
297 # commit, filter and clean up
298 date=$(eval "$DATE")
299 author=$(eval "$AUTHOR")
300 title=$(eval "$TITLE")
301 git commit ${AUTHOR:+--author "$author"} -m "$title"${BODY:+"$NL$NL$BODY"} ${DATE:+--date "$date"} $CIARGS
302 if [ -n "$FILTERHEAD" ]
303 then
304 IFS=$NL
305 git filter-branch $FILTERHEAD -- HEAD^..HEAD
306 rm -R "$(git rev-parse --git-dir)"/refs/original/
307 IFS=$OLDIFS
309 if [ -n "$TAG" ]
310 then
311 tagmessage=$(eval "$TAG" | LC_ALL=C tail -n +2)
312 tagname=$(eval "$TAG" | LC_ALL=C head -1)
313 git tag ${tagmessage:+-m "$tagmessage"} "$tagname"
315 if [ -n "$COMMAND" ]
316 then
317 eval "$COMMAND"
319 unset title date author tagname tagmessage
320 done