archives2git: --strip-ext: new option
[archives2git.git] / archives2git
blob466dbb5fdc8796c901f549f686e61afef43c2f45
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 GIT_AUTHOR_NAME
40 GIT_AUTHOR_EMAIL
41 GIT_COMMITTER_NAME
42 GIT_COMMITTER_EMAIL
43 FILES
44 ~/.archives2gitrc
45 This file is sourced by the script. An example file is distributed.
46 EXAMPLES
47 Some possible options:
48 $ archives2git \$(ls ../oldproject-*.tgz | sort -V) \\
49 # keep (only) the .gitignore file
50 --keep-filter 'test x.gitignore = x"\$file"'
51 --keep-filter 'echo "\$file" | grep -q -f \$HOME/filepats'
52 # suppress the version info from the root directory in the archive
53 --rename 'echo "\${file%-*}"'
54 # allow to add ignored files
55 --gitadd-arg -f
56 # use the archive modification time as the author date (with GNU date)
57 --date 'LC_ALL=C date -r "\$arch"'
58 # do not include the path components in the commit title
59 --title 'echo "\${arch##*/}"'
60 # interactively edit each commit message
61 --gitci-arg "--edit"
62 # keep only the files of the project in the root of the tree
63 --gitfilter $'--subdirectory-filter\noldproject'
64 # move the project's files out of their subdir (while keeping other files)
65 --gitfilter $'--tree-filter\nmv oldproject/.* .; mv oldproject/* .'
66 See also the example configuration file.
67 AUTHOR
68 $PROGRAM_NAME was written by G.raud Meyer.
69 SEE ALSO
70 git-commit-tree(1), git_load_dirs(1), git-rev-parse(1)
71 HelpMessage
73 OPTS_SPEC="\
74 $PROGRAM_USAGE
76 unpack= command to extract the files of the \$arch in \$TMPDIR
77 strip-ext= sh code to strip the filename extension from \$file
78 repo= work tree subdir (current dir by default)
79 rename= sh code to rename the current \$file of the root
80 keep-filter= sh code to decide whether not to remove the \$file of the root
81 gitadd-arg= argument to pass to git-add
82 date= sh code printing the commit author date for the current \$arch
83 author= sh code printing the commit author name for the current \$arch
84 title= sh code printing the commit title (and an optional body start)
85 body= string appended to the title as a new paragraph
86 gitci-arg= argument to pass to git-commit
87 gitfilter= line separated arguments to pass to git-filter
88 tag tag the commits with the default tagname
89 tagname= tag the commits with the name printed by the given sh code
90 command= sh code to eval after having commited and tagged
91 debug enable the verbose printing of what is done
92 h,help print a usage message and exit
93 helpm print the manual and exit
94 man view the manual in the pager and exit
95 version print the version information and exit
98 # helper functions
99 helpm_options () {
100 printf %s "$OPTS_SPEC" | LC_ALL=C awk '/^--$/ { parse=1; next }
101 function opt(spec) {
102 if (sub(/^[a-zA-Z0-9][=*?!]{0,3}(,|$)/, "-&", spec)) sub(/^-.[=*?!]{0,3},/, "& --", spec)
103 else sub(/^/, "--&", spec)
104 return spec
106 parse { print " \t" opt($1); sub(/^[^ \t]*[ \t]*/,""); if ($0) print "\t\t" $0 }'
108 get_version () {
109 _file=$1; shift
110 _base=$( printf "%s\n" "$_file" |
111 LC_ALL=C sed -r -e '$s;(([0-9]{1,}\.)|[-_])[0-9]{1,}([-_.]{0,1}([0-9]{1,}|[ab]|alpha|beta|gamma))*$;;' )
112 _version=${_file#"$_base"}
113 _version=${_version#"-"}; _version=${_version#"."}; _version=${_version#"_"}
114 printf "%s\n" "${_version}"
116 strip_extension () {
117 printf "%s\n" "$1" | LC_ALL=C sed -r \
118 -e '$s;^([^/]*/)*([^/]{1,})/{0,1}$;\2;' \
119 -e '$s;\.([Zz]|lzo|bz|gz|bz2|xz|lzma|7z|lz|rz|lrz)$;;' \
120 -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)$;;' \
121 -e '$s;\.(orig)$;;'
124 # default parameters
125 UNPACK=
126 STRIP_EXT='strip_extension "$arch"'
127 RENAME='' # $file $arch $archname
128 FILTER='false' # $file $arch $archname
129 DATE='' # $arch $archname
130 TITLE='echo "$arch"' # $arch $archname
131 BODY="" # message taken as is
132 ADDARGS=
133 CIARGS=
134 FILTERHEAD= # line separated arguments
135 TAG=
136 TAGDEF='printf version/; get_version "$archname"'
137 COMMAND=
138 # config (file, environment, command line)
139 [ -f "$HOME"/.archives2gitrc ] && . "$HOME"/.archives2gitrc
140 GIT_WORK_TREE=${GIT_WORK_TREE-.}
141 # may be a subdir of the top level
142 parsing=$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@"); rc=$?
143 eval "$parsing"
144 if [ $rc -ne 0 ]
145 then printf %s "$parsing" | grep '^EOF' >/dev/null && exit 0 || exit $rc
147 while [ $# -gt 0 ]
149 case $1 in
150 --unpack)
151 shift; UNPACK=$1 ;;
152 --strip-ext)
153 shift; STRIP_EXT=$1 ;;
154 --repo)
155 shift; GIT_WORK_TREE=$1 ;;
156 --rename)
157 shift; RENAME=$1 ;;
158 --keep-filter)
159 shift; FILTER=$1 ;;
160 --gitadd-arg)
161 shift; ADDARGS="$ADDARGS $1" ;;
162 --date)
163 shift; DATE=$1 ;;
164 --author)
165 shift; AUTHOR=$1 ;;
166 --title)
167 shift; TITLE=$1 ;;
168 --body)
169 shift; BODY=$1 ;;
170 --gitci-arg)
171 shift; CIARGS="$CIARGS $1" ;;
172 --gitfilter)
173 shift; FILTERHEAD=$1 ;;
174 --tag)
175 TAG=$TAGDEF ;;
176 --tagname)
177 shift; TAG=$1 ;;
178 --command)
179 shift; COMMAND=$1 ;;
180 --debug)
181 set -x ;;
182 --helpm)
183 help_message
184 exit ;;
185 --man)
186 help_message | helpm2pod --man - ARCHIVES2GIT
187 exit ;;
188 --version)
189 echo "$PROGRAM_NAME version $PROGRAM_VERSION"
190 exit ;;
191 --?*)
192 echo >&2 "$PROGRAM_NAME: internal error: invalid option $1"
193 exit 255 ;;
195 shift; break ;;
197 break ;;
198 esac
199 shift
200 done
201 # setup
202 NL='
204 trap 'rmdir "$TMPDIR"' EXIT
205 TMPDIR=$(mktemp -d) ||
207 echo >&2 "$PROGRAM_NAME: error: cannot create a temporary directory"
208 exit 253
210 WRKDIR=$(pwd)
211 # git repository check
212 cd "$GIT_WORK_TREE" &&
213 { test -d .git || git rev-parse --git-dir >/dev/null; } ||
215 echo >&2 "$PROGRAM_NAME: error: not in a git repository ($(pwd))"
216 exit 255
218 test -z "$(git status --porcelain)" &&
219 git update-index -q --refresh ||
221 echo >&2 "$PROGRAM_NAME: error: unstaged files or dirty index"
222 exit 254
225 # main
226 set -e
227 OLDIFS=$IFS
228 for arch
230 # put the files in the archive dir
231 cd "$WRKDIR"
232 if [ -d "$arch" ]
233 then
234 cp -R "$arch" "$TMPDIR"
235 elif [ -n "$UNPACK" ]
236 then
237 eval "$UNPACK"
238 else
239 aunpack -X "$TMPDIR" "$arch"
241 archdir=$TMPDIR
242 archname=$(eval "$STRIP_EXT")
243 # remove (almost) everything from the repository dir
244 cd "$GIT_WORK_TREE"
245 for file in * .*
247 [ -e "$file" ] || [ -h "$file" ] || continue
248 [ x"$file" = x"." ] || [ x"$file" = x".." ] || [ x"$file" = x".git" ] && continue
249 eval "$FILTER" || { git rm -r "$file" || rm -R "$file"; }
250 done
251 # move the content of the temp dir to the repository and stage it
252 for file in "$archdir"/* "$archdir"/.*
254 [ -e "$file" ] || [ -h "$file" ] || continue
255 file=${file#$archdir/}
256 [ x"$file" = x"." ] || [ x"$file" = x".." ] && continue
257 [ -n "$RENAME" ] && name=$(eval "$RENAME") || name=$file
258 if [ -n "$name" ]
259 then
260 [ ! -e ./"$name" ] || rm -R ./"$name"
261 # remove conflicting file (previously kept or renamed to the same name)
262 mv "$archdir"/"$file" ./"$name"
263 git add $ADDARGS ./"$name"
264 else
265 rm -R "$archdir"/"$file"
267 done
268 unset file
269 # commit, filter and clean up
270 date=$(eval "$DATE")
271 author=$(eval "$AUTHOR")
272 title=$(eval "$TITLE")
273 git commit ${AUTHOR:+--author "$author"} -m "$title"${BODY:+"$NL$NL$BODY"} ${DATE:+--date "$date"} $CIARGS
274 if [ -n "$FILTERHEAD" ]
275 then
276 IFS=$NL
277 git filter-branch $FILTERHEAD -- HEAD^..HEAD
278 rm -R "$(git rev-parse --git-dir)"/refs/original/
279 IFS=$OLDIFS
281 if [ -n "$TAG" ]
282 then
283 tagmessage=$(eval "$TAG" | LC_ALL=C tail -n +2)
284 tagname=$(eval "$TAG" | LC_ALL=C head -1)
285 git tag ${tagmessage:+-m "$tagmessage"} "$tagname"
287 if [ -n "$COMMAND" ]
288 then
289 eval "$COMMAND"
291 unset title date author tagname tagmessage
292 done