-
Notifications
You must be signed in to change notification settings - Fork 2
/
git-submodule-rewrite.sh
executable file
·246 lines (207 loc) · 7.33 KB
/
git-submodule-rewrite.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#!/usr/bin/env bash
# This script builds on the excellent work by Lucas Jenß, described in his blog
# post "Integrating a submodule into the parent repository", but automates the
# entire process and cleans up a few other corner cases.
# https://x3ro.de/2013/09/01/Integrating-a-submodule-into-the-parent-repository.html
function usage(){
echo "Usage: $0 <submodule-name> [<submodule-branch>]"
echo "Merge a single branch of <submodule-name> into a repo, retaining file history."
echo "If provided then <submodule-branch> will be merged, otherwise master."
echo ""
echo "options:"
echo " -h, --help Print this message"
echo " -f, --force Skip the warning message (use with caution)"
echo " -v, --verbose Display verbose output"
}
function abort {
echo "$(tput setaf 1)$1$(tput sgr0)"
exit 1
}
function request_confirmation {
read -p "$(tput setaf 4)$1 (y/n) $(tput sgr0)"
[ "$REPLY" == "y" ] || abort "Aborted!"
}
function warn() {
cat << EOF
This script will convert your "${sub}" git submodule into
a simple subdirectory in the parent repository while retaining all
contents, file history and its own submodules.
The script will:
* delete the ${sub} submodule configuration from .gitmodules and
.git/config and commit it.
* rewrite the entire history of the ${sub} submodule so that all
paths are prefixed by ${path}.
This ensures that git log will correctly follow the original file
history.
* merge the submodule's tags into its parent repository and commit
each tag merge individually.
(only those tags are considered which are reachable from
the tip of ${sub}/${branch})
* merge the submodule into its parent repository and commit it.
* reinstate any of the submodule's own submodules as part of the parent
repository
NOTE: This script might completely garble your repository, so PLEASE apply
this only to a fresh clone of the repository where it does not matter if
the repo is destroyed. It would be wise to keep a backup clone of your
repository, so that you can reconstitute it if need be. You have been
warned. Use at your own risk.
EOF
request_confirmation "Do you want to proceed?"
}
function git_version_lte() {
OP_VERSION=$(printf "%03d%03d%03d%03d" $(echo "$1" | tr '.' '\n' | head -n 4))
GIT_VERSION=$(git version)
GIT_VERSION=$(printf "%03d%03d%03d%03d" $(echo "${GIT_VERSION#git version }" | sed -E "s/([0-9.]*).*/\1/" | tr '.' '\n' | head -n 4))
echo -e "${GIT_VERSION}\n${OP_VERSION}" | sort | head -n1
[ ${GIT_VERSION} -le ${OP_VERSION} ]
}
# Convert a url to an absolute url
#
# Parameters:
# $1: The url to check
# $2: The base url to use if $1 is a relative path
#
# Returns an absolute url
function absolute_url {
local url=$1
local base=$2
if [[ $url =~ \.\. ]]; then
echo "$base/$(basename $url)"
else
echo $url
fi
}
function main() {
if [ "${skip_warn}" != "true" ]; then
warn
fi
if [ "${verbose}" == "true" ]; then
set -x
fi
# Remove submodule and commit
git config -f .gitmodules --remove-section "submodule.${sub}"
if git config -f .git/config --get "submodule.${sub}.url"; then
git config -f .git/config --remove-section "submodule.${sub}"
fi
rm -rf "${path}"
git add -A .
git commit -m "Remove submodule ${sub}"
rm -rf ".git/modules/${sub}"
# Rewrite submodule history
local tmpdir="$(mktemp -d -t submodule-rewrite-XXXXXX)"
git clone -b "${branch}" "${url}" "${tmpdir}"
# Be sure to get all tags as well as we will mege them later on
git fetch --tags
pushd "${tmpdir}"
local tab="$(printf '\t')"
local filter="git ls-files -s | sed \"s:${tab}:${tab}${path}/:\" | GIT_INDEX_FILE=\${GIT_INDEX_FILE}.new git update-index --index-info && mv \${GIT_INDEX_FILE}.new \${GIT_INDEX_FILE} || true"
git filter-branch --index-filter "${filter}" HEAD
popd
# Merge in rewritten submodule history
git remote add "${sub}" "${tmpdir}"
git fetch --tags "${sub}"
if git_version_lte 2.8.4
then
# Previous to git 2.9.0 the parameter would yield an error
ALLOW_UNRELATED_HISTORIES=""
else
# From git 2.9.0 this parameter is required
ALLOW_UNRELATED_HISTORIES="--allow-unrelated-histories"
fi
# First merge all tags
# We only consider tags reachable from the specified submodule's
# branch. All other tags will be ignored.
for submodule_tag in $(git tag --list --merged "${sub}/${branch}")
do
git merge -s ours -m "Merge submodule tag ${submodule_tag} for ${sub}/${branch}" ${ALLOW_UNRELATED_HISTORIES} "${submodule_tag}"
done
# Now merge actual files
git merge -s ours --no-commit ${ALLOW_UNRELATED_HISTORIES} "${sub}/${branch}"
rm -rf tmpdir
# Add submodule content
git clone -b "${branch}" "${url}" "${path}"
# Transfer its own submodules to the parent
add_submod_cmds=""
if [ -f ${path}/.gitmodules ]; then
sub_names=$(git config -f ${path}/.gitmodules --get-regex path | sed 's/.* \(.*\)$/\1/g' || true)
for sub_name in ${sub_names}; do
sub_branch=$(git config -f ${path}/.gitmodules --get "submodule.${sub_name}.branch") || true
[ -n "${sub_branch}" ] && sub_branch="-b ${sub_branch}"
sub_path=$(git config -f ${path}/.gitmodules --get "submodule.${sub_name}.path")
sub_url=$(git config -f ${path}/.gitmodules --get "submodule.${sub_name}.url")
# remove the sub-submodule (which should be empty) and cache the command to reinstate it
rmdir ${path}/${sub_path}
add_submod_cmds="$add_submod_cmds git submodule add ${sub_branch} --name ${sub_name} -- ${sub_url} ${path}/${sub_path} ; "
done
fi
rm -rf "${path}/.git" "${path}/.gitmodules"
git add "${path}"
if [ -n "${add_submod_cmds}" ]; then
bash -c "${add_submod_cmds}"
fi
git commit -m "Merge submodule contents for ${sub}/${branch}"
git config -f .git/config --remove-section "remote.${sub}"
set +x
echo "$(tput setaf 2)Submodule merge complete. Push changes after review.$(tput sgr0)"
}
set -euo pipefail
declare verbose=false
declare skip_warn=false
while [ $# -gt 0 ]; do
case "$1" in
(-h|--help)
usage
exit 0
;;
(-v|--verbose)
verbose=true
;;
(-f|--force)
skip_warn=true
;;
(*)
break
;;
esac
shift
done
declare sub="${1:-}"
declare branch="${2:-master}"
if [ -z "${sub}" ]; then
>&2 echo "Error: No submodule specified"
usage
exit 1
fi
shift
if [ -n "${1:-}" ]; then
shift
fi
if [ -n "${1:-}" ]; then
>&2 echo "Error: Unknown option: ${1:-}"
usage
exit 1
fi
if ! [ -d ".git" ]; then
>&2 echo "Error: No git repository found. Must be run from the root of a git repository"
usage
exit 1
fi
declare path="$(git config -f .gitmodules --get "submodule.${sub}.path")"
declare superproject_dir="$(dirname $(git config --get remote.origin.url))"
declare url=$(absolute_url $(git config -f .gitmodules --get "submodule.${sub}.url") $superproject_dir)
if [ -z "${path}" ]; then
>&2 echo "Error: Submodule not found: ${sub}"
usage
exit 1
fi
if [ -z "${superproject_dir}" ]; then
>&2 echo "Error: Could not determine the remote origin for this superproject: ${superproject_dir}"
usage
exit 1
fi
if ! [ -d "${path}" ]; then
>&2 echo "Error: Submodule path not found: ${path}"
usage
exit 1
fi
main