version/0.3
[archives2git.git] / archives2git
blob31ee5bc63597e718b60dc7068c406d72a567e59e
1 #!/bin/sh
2 # Copyright © 2013,2014,2017,2018 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.3"
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
27 that of an <archive> file (or of another <directory>) and commits the
28 changes.
29 OPTIONS
30 The options are parsed by git-rev-parse(1) with the option --parseopt.
31 $( helpm_options )
32 SHELL VARIABLES
33 The following shell variables are usable if appropriate in the shell
34 code snippets passed as command line options: \$arch, \$archdir,
35 \$archname, \$file, \$date, \$author, \$title, \$tagname.
36 SHELL FUNCTIONS
37 The following shell functions are defined in the script and are usable
38 in the shell code snippets: get_version(filename).
39 ENVIRONMENT
40 ARCHIVES2GIT_ENV
41 After parameter and command substitutions and arithmetic
42 expansion, it gives the path of the config file to source. If
43 unset the default configuration file is sourced instead.
44 GIT_AUTHOR_NAME
45 GIT_AUTHOR_EMAIL
46 GIT_COMMITTER_NAME
47 GIT_COMMITTER_EMAIL
48 FILES
49 ~/.archives2gitrc
50 This file is sourced by the script. An example file is
51 distributed.
52 .git/archives2gitrc
53 This repository configuration file is preferred over the global
54 one if it exists.
55 EXAMPLES
56 Some possible options:
57 $ ARCHIVES2GIT_ENV=/dev/null \\
58 archives2git \$(ls ../oldproject-*.tgz | sort -V) \\
59 # keep (only) the .gitignore file
60 --keep-filter 'test x.gitignore = x"\$file"'
61 --keep-filter 'echo "\$file" | grep -q -f \$HOME/filepats'
62 # suppress the version info from the root directory in the archive
63 --rename 'echo "\${file%-*}"'
64 # allow to add ignored files
65 --gitadd-arg -f
66 # use the archive modification time as the author date (with GNU date)
67 --date 'LC_ALL=C date -r "\$arch"'
68 # do not include the path components in the commit title
69 --title 'echo "\${arch##*/}"'
70 # interactively edit each commit message
71 --gitci-arg "--edit"
72 # keep only the files of the project in the root of the tree
73 --gitfilter $'--subdirectory-filter\noldproject'
74 # move the project's files out of their subdir (while keeping other files)
75 --gitfilter $'--tree-filter\nmv oldproject/.* .; mv oldproject/* .'
76 See also the example configuration file.
77 BUGS
78 Trailing whitespaces in filenames are not supported.
79 AUTHOR
80 $PROGRAM_NAME was written by G.raud Meyer.
81 SEE ALSO
82 git-commit-tree(1), git_load_dirs(1), git-rev-parse(1)
83 HelpMessage
85 OPTS_SPEC="\
86 $PROGRAM_USAGE
88 unpack= command to extract the files of the \$arch in \$TMPDIR
89 strip-ext= sh code to strip the filename extension from \$file
90 repo= work tree subdir (current dir by default)
91 rename= sh code to rename the current \$file of the root
92 keep-filter= sh code to decide whether not to remove the \$file of the root
93 gitadd-arg= argument to pass to git-add
94 date= sh code printing the commit author date for the current \$arch
95 author= sh code printing the commit author name for the current \$arch
96 title= sh code printing the commit title (and an optional body start)
97 body= string appended to the title as a new paragraph
98 gitci-arg= argument to pass to git-commit
99 gitfilter= line separated arguments to pass to git-filter
100 tag tag the commits with the default tagname
101 tagname= tag the commits with the name printed by the given sh code
102 command= sh code to eval after having commited and tagged
103 debug enable the verbose printing of what is done
104 keep-tempdir do not delete the temporary extraction dirs
105 h,help print a usage message and exit
106 helpm print the manual and exit
107 man view the manual in the pager and exit
108 version print the version information and exit
111 # helper functions
112 helpm_options () {
113 printf %s "$OPTS_SPEC" | LC_ALL=C awk '/^--$/ { parse=1; next }
114 function opt(spec) {
115 if (sub(/^[a-zA-Z0-9][=*?!]{0,3}(,|$)/, "-&", spec)) sub(/^-.[=*?!]{0,3},/, "& --", spec)
116 else sub(/^/, "--&", spec)
117 return spec
119 parse { print " \t" opt($1); sub(/^[^ \t]*[ \t]*/,""); if ($0) print "\t\t" $0 }'
121 get_version () {
122 _file=$1; shift
123 _base=$( printf "%s\n" "$_file" |
124 LC_ALL=C sed -r -e '$s;(([0-9]{1,}\.)|[-_])[0-9]{1,}([-_.]{0,1}([0-9]{1,}|[ab]|alpha|beta|gamma))*$;;' )
125 _version=${_file#"$_base"}
126 _version=${_version#"-"}; _version=${_version#"."}; _version=${_version#"_"}
127 printf "%s\n" "${_version}"
129 warn () { echo >&2 "$@"; }
130 fail () { _rc=$1; shift; warn "$PROGRAM_NAME:" "$@"; exit $_rc; }
131 strip_extension () {
132 printf "%s\n" "$1" | LC_ALL=C sed -r \
133 -e '$s;^([^/]*/)*([^/]{1,})/{0,1}$;\2;' \
134 -e '$s;\.([Zz]|lzo|bz|gz|bz2|xz|lzma|7z|lz|rz|lrz)$;;' \
135 -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)$;;' \
136 -e '$s;\.(orig)$;;'
138 NL='
141 # default parameters
142 UNPACK=
143 STRIP_EXT='strip_extension "$arch"'
144 RENAME='' # $file $arch $archname
145 FILTER='false' # $file $arch $archname
146 DATE='' # $arch $archname
147 TITLE='echo "$arch"' # $arch $archname
148 BODY="" # message taken as is
149 ADDARGS=
150 CIARGS=
151 FILTERHEAD= # line separated arguments
152 TAG=
153 TAGDEF='printf version/; get_version "$archname"'
154 COMMAND=
155 KEEP_TEMPDIR=
156 # config (file, environment, command line)
157 if [ -n "${ARCHIVES2GIT_ENV+set}" ]
158 then
159 dquotedexpr=$( printf "%s" "$ARCHIVES2GIT_ENV" | LC_ALL=C sed 's/"/\\"/g' )
160 # double quoting still allows backslash escapes (as well as
161 # substitutions and expansions)
162 configfile=
163 (eval : '${configfile:="'"$dquotedexpr"'"}') &&
164 eval : '${configfile:="'"$dquotedexpr"'"}'
165 # use := instead of = as the substitution for more portability (which
166 # is not that sensible since the + substitution is not implemented
167 # either when = is not)
168 elif
169 gitdir=$(git rev-parse --git-dir) &&
170 [ -f "$gitdir"/archives2gitrc ]
171 then
172 configfile=$gitdir/archives2gitrc
173 else
174 configfile=$HOME/.archives2gitrc
176 [ -r "$configfile" ] && . "$configfile"
177 GIT_WORK_TREE=${GIT_WORK_TREE-.}
178 # may be a subdir of the top level
179 parsing=$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@"); rc=$?
180 eval "$parsing"
181 if [ $rc -ne 0 ]
182 then printf %s "$parsing" | grep '^EOF' >/dev/null && exit 0 || exit $rc
184 while [ $# -gt 0 ]
186 case $1 in
187 --unpack)
188 shift; UNPACK=$1 ;;
189 --strip-ext)
190 shift; STRIP_EXT=$1 ;;
191 --repo)
192 shift; GIT_WORK_TREE=$1 ;;
193 --rename)
194 shift; RENAME=$1 ;;
195 --keep-filter)
196 shift; FILTER=$1 ;;
197 --gitadd-arg)
198 shift; ADDARGS="$ADDARGS$NL$1" ;;
199 --date)
200 shift; DATE=$1 ;;
201 --author)
202 shift; AUTHOR=$1 ;;
203 --title)
204 shift; TITLE=$1 ;;
205 --body)
206 shift; BODY=$1 ;;
207 --gitci-arg)
208 shift; CIARGS="$CIARGS$NL$1" ;;
209 --gitfilter)
210 shift; FILTERHEAD=$1 ;;
211 --tag)
212 TAG=$TAGDEF ;;
213 --tagname)
214 shift; TAG=$1 ;;
215 --command)
216 shift; COMMAND=$1 ;;
217 --debug)
218 set -x ;;
219 --keep-tempdir)
220 KEEP_TEMPDIR=yes ;;
221 --helpm)
222 help_message
223 exit ;;
224 --man)
225 help_message | helpm2pod --man - ARCHIVES2GIT
226 exit ;;
227 --version)
228 echo "$PROGRAM_NAME version $PROGRAM_VERSION"
229 exit ;;
230 --?*)
231 fail 255 "internal error: invalid option $1" ;;
233 shift; break ;;
235 break ;;
236 esac
237 shift
238 done
239 # setup
240 [ -z "$KEEP_TEMPDIR" ] && trap 'rm -rf "$TMPDIR"' EXIT INT QUIT TERM
241 TMPDIR=$(mktemp -d) ||
242 fail 253 "error: cannot create a temporary directory"
243 WRKDIR=$(pwd)
244 # git repository check
245 cd "$GIT_WORK_TREE" &&
246 { test -d .git || git rev-parse --git-dir >/dev/null; } ||
247 fail 255 "error: not in a git repository ($(pwd))"
248 test -z "$(git status --porcelain)" &&
249 git update-index -q --refresh ||
250 fail 254 "error: unstaged files or dirty index"
252 # main
253 set -e
254 (set -o pipefail) 2>/dev/null && set -o pipefail
255 OLDIFS=$IFS
256 for arch
258 # put the files in the archive dir
259 cd "$WRKDIR"
260 if [ -d "$arch" ]
261 then
262 cp -R "$arch" "$TMPDIR"
263 elif [ -n "$UNPACK" ]
264 then
265 (eval "$UNPACK")
266 else
267 aunpack -X "$TMPDIR" "$arch"
269 archdir=$TMPDIR
270 archname=$(eval "$STRIP_EXT") || fail 32 "\$STRIP_EXT failed"
271 # remove (almost) everything from the repository dir
272 cd "$GIT_WORK_TREE"
273 for file in * .*
275 [ -e "$file" ] || [ -h "$file" ] || continue
276 [ x"$file" = x"." ] || [ x"$file" = x".." ] ||
277 [ x"$file" = x".git" ] && continue
278 (eval "$FILTER") || { git rm -r "$file" || rm -R "$file"; }
279 done
280 # move the content of the temp dir to the repository and stage it
281 for file in "$archdir"/* "$archdir"/.*
283 [ -e "$file" ] || [ -h "$file" ] || continue
284 file=${file#$archdir/}
285 [ x"$file" = x"." ] || [ x"$file" = x".." ] && continue
286 [ -n "$RENAME" ] && name=$(eval "$RENAME") || name=$file
287 if [ -n "$name" ]
288 then
289 if [ -e ./"$name" ]
290 then
291 # remove conflicting file (previously kept or renamed
292 # to the same name)
293 rm -R ./"$name"
294 warn "Warning: conflicting file removed: \`$name'"
296 mv "$archdir"/"$file" ./"$name"
297 IFS=$NL; set -f
298 git add $ADDARGS ./"$name"
299 set +f; IFS=$OLDIFS
300 else
301 rm -R "$archdir"/"$file"
303 unset name
304 done
305 unset file
306 # commit, filter and clean up
307 date=$(eval "$DATE") || fail 32 "\$DATE failed with $?"
308 author=$(eval "$AUTHOR") || fail 32 "\$AUTHOR failed with $?"
309 title=$(eval "$TITLE") || fail 32 "\$TITLE failed with $?"
310 IFS=$NL; set -f
311 git commit ${AUTHOR:+--author$NL"$author"} ${DATE:+--date$NL"$date"} \
312 -m "$title"${BODY:+"$NL$NL$BODY"} $CIARGS
313 if [ -n "$FILTERHEAD" ]
314 then
315 git filter-branch $FILTERHEAD -- HEAD^..HEAD
316 rm -R "$(git rev-parse --git-dir)"/refs/original/
318 set +f; IFS=$OLDIFS
319 if [ -n "$TAG" ]
320 then
321 tag=$(eval "$TAG") || fail 32 "\$TAG failed with $?"
322 tagmessage=$(printf %s "$tag" | LC_ALL=C tail -n +2) || fail 32 "tail failed with $?"
323 tagname=$(printf %s "$tag" | LC_ALL=C head -1) || fail 32 "head failed with $?"
324 git tag ${tagmessage:+-m "$tagmessage"} "$tagname"
326 if [ -n "$COMMAND" ]
327 then
328 (eval "$COMMAND")
330 unset title date author tag tagname tagmessage
331 done