1#=============================================================================
2# Copyright 2015-2016 Kitware, Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#=============================================================================
16
17########################################################################
18# Script for updating third party packages.
19#
20# This script should be sourced in a project-specific script which sets
21# the following variables:
22#
23#   name
24#       The name of the project.
25#   ownership
26#       A git author name/email for the commits.
27#   subtree
28#       The location of the thirdparty package within the main source
29#       tree.
30#   repo
31#       The git repository to use as upstream.
32#   tag
33#       The tag, branch or commit hash to use for upstream.
34#   shortlog
35#       Optional.  Set to 'true' to get a shortlog in the commit message.
36#
37# Additionally, an "extract_source" function must be defined. It will be
38# run within the checkout of the project on the requested tag. It should
39# should place the desired tree into $extractdir/$name-reduced. This
40# directory will be used as the newest commit for the project.
41#
42# For convenience, the function may use the "git_archive" function which
43# does a standard "git archive" extraction using the (optional) "paths"
44# variable to only extract a subset of the source tree.
45########################################################################
46
47########################################################################
48# Utility functions
49########################################################################
50git_archive () {
51    git archive --worktree-attributes --prefix="$name-reduced/" HEAD -- $paths | \
52        tar -C "$extractdir" -x
53}
54
55disable_custom_gitattributes() {
56    pushd "${extractdir}/${name}-reduced"
57    # Git does not allow custom attributes in a subdirectory where we
58    # are about to merge the `.gitattributes` file, so disable them.
59    sed -i '/^\[attr\]/ {s/^/#/;}' .gitattributes
60    popd
61}
62
63die () {
64    echo >&2 "$@"
65    exit 1
66}
67
68warn () {
69    echo >&2 "warning: $@"
70}
71
72readonly regex_date='20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
73readonly basehash_regex="$name $regex_date ([0-9a-f]*)"
74readonly toplevel_dir="$( git rev-parse --show-toplevel )"
75
76cd "$toplevel_dir"
77
78########################################################################
79# Sanity checking
80########################################################################
81[ -n "$name" ] || \
82    die "'name' is empty"
83[ -n "$ownership" ] || \
84    die "'ownership' is empty"
85[ -n "$subtree" ] || \
86    die "'subtree' is empty"
87[ -n "$repo" ] || \
88    die "'repo' is empty"
89[ -n "$tag" ] || \
90    die "'tag' is empty"
91
92# Check for an empty destination directory on disk.  By checking on disk and
93# not in the repo it allows a library to be freshly re-inialized in a single
94# commit rather than first deleting the old copy in one commit and adding the
95# new copy in a seperate commit.
96if [ ! -d "$(git rev-parse --show-toplevel)/$subtree" ]; then
97    readonly basehash=""
98else
99    readonly basehash="$( git rev-list --author="$ownership" --grep="$basehash_regex" -n 1 HEAD )"
100fi
101readonly upstream_old_short="$( git cat-file commit "$basehash" | sed -n '/'"$basehash_regex"'/ {s/.*(//;s/)//;p;}' | egrep '^[0-9a-f]+$' )"
102
103[ -n "$basehash" ] || \
104    warn "'basehash' is empty; performing initial import"
105readonly do_shortlog="${shortlog-false}"
106
107readonly workdir="$PWD/work"
108readonly upstreamdir="$workdir/upstream"
109readonly extractdir="$workdir/extract"
110
111[ -d "$workdir" ] && \
112    die "error: workdir '$workdir' already exists"
113
114trap "rm -rf '$workdir'" EXIT
115
116# Get upstream
117git clone "$repo" "$upstreamdir"
118
119if [ -n "$basehash" ]; then
120    # Remove old worktrees
121    git worktree prune
122    # Use the existing package's history
123    git worktree add "$extractdir" "$basehash"
124    # Clear out the working tree
125    pushd "$extractdir"
126    git ls-files | xargs rm -v
127    find . -type d -empty -delete
128    popd
129else
130    # Create a repo to hold this package's history
131    mkdir -p "$extractdir"
132    git -C "$extractdir" init
133fi
134
135# Extract the subset of upstream we care about
136pushd "$upstreamdir"
137git checkout "$tag"
138readonly upstream_hash="$( git rev-parse HEAD )"
139readonly upstream_hash_short="$( git rev-parse --short=8 "$upstream_hash" )"
140readonly upstream_datetime="$( git rev-list "$upstream_hash" --format='%ci' -n 1 | grep -e "^$regex_date" )"
141readonly upstream_date="$( echo "$upstream_datetime" | grep -o -e "$regex_date" )"
142if $do_shortlog && [ -n "$basehash" ]; then
143    readonly commit_shortlog="
144
145Upstream Shortlog
146-----------------
147
148$( git shortlog --no-merges --abbrev=8 --format='%h %s' "$upstream_old_short".."$upstream_hash" )"
149else
150    readonly commit_shortlog=""
151fi
152extract_source || \
153    die "failed to extract source"
154popd
155
156[ -d "$extractdir/$name-reduced" ] || \
157    die "expected directory to extract does not exist"
158readonly commit_summary="$name $upstream_date ($upstream_hash_short)"
159
160# Commit the subset
161pushd "$extractdir"
162mv -v "$name-reduced/"* .
163rmdir "$name-reduced/"
164git add -A .
165git commit -n --author="$ownership" --date="$upstream_datetime" -F - <<-EOF
166$commit_summary
167
168Code extracted from:
169
170    $repo
171
172at commit $upstream_hash ($tag).$commit_shortlog
173EOF
174git branch -f "upstream-$name"
175popd
176
177# Merge the subset into this repository
178if [ -n "$basehash" ]; then
179    git merge --log -s recursive "-Xsubtree=$subtree/" --no-commit "upstream-$name"
180else
181    # Note: on Windows 'git merge --help' will open a browser, and the check
182    # will fail, so use the flag by default.
183    unrelated_histories_flag=""
184    if git --version | grep -q windows; then
185        unrelated_histories_flag="--allow-unrelated-histories "
186    elif git merge --help | grep -q -e allow-unrelated-histories; then
187        unrelated_histories_flag="--allow-unrelated-histories "
188    fi
189    readonly unrelated_histories_flag
190
191    git fetch "$extractdir" "+upstream-$name:upstream-$name"
192    git merge --log -s ours --no-commit $unrelated_histories_flag "upstream-$name"
193    git read-tree -u --prefix="$subtree/" "upstream-$name"
194fi
195git commit --no-edit
196git branch -d "upstream-$name"
197