This repository has been archived by the owner on Dec 31, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathcrimport
executable file
·392 lines (333 loc) · 10.1 KB
/
crimport
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/bin/bash
#
# crimport REPO_GITHUB_ACCOUNT REPO_NAME [GERRIT_USER]: imports the given
# repository into gerrit. See the usage message for details.
#
#
# Configuration
#
# DNS name or IP address of Gerrit server
igp_gerrit_host=cr.joyent.us
# Group name for Gerrit superusers
igp_gerrit_group="Temporary Git Superusers"
# Temporary directory for cloned project
igp_tmproot="${TMPDIR-/var/tmp}/crimport-$$"
# URL for accessing Github API
igp_github_api="https://api.github.com"
# Location of file containing user's Github API token
igp_github_token_file=~/.github-api-token
# User that Gerrit uses to push to Github
igp_github_gerrit_user="joyent-automation"
# Slug for the "Joyent Engineering" team
igp_github_eng_slug="joyent-engineering"
#
# Runtime state
#
#
# SSH host string. This is generally the same as igp_gerrit_host, but may
# include a "username@" if specified on the command line.
#
igp_gerrit_sshhost=
#
# Source repository: GitHub account and repository name.
# These are filled in from the command-line arguments.
#
igp_gh_account=
igp_gh_repo=
# Gerrit project name. This is constructed from the GitHub parameters.
igp_gerrit_project=
# Gerrit username. This is reported by the server.
igp_gerrit_username=
# Directory into which the source repository is cloned.
igp_clonedir=
function usage
{
cat <<EOF >&2
usage: crimport [-A] REPO_GITHUB_ACCOUNT REPO_NAME [GERRIT_USER]
This tool copies a GitHub repository at
https://github.com/REPO_GITHUB_ACCOUNT/REPO_NAME
to a new project on the Joyent Gerrit server at $igp_gerrit_host. The new
Gerrit project will be configured to replicate back to GitHub.
REPO_GITHUB_ACCOUNT may be either an individual GitHub account or a GitHub
organization.
If GERRIT_USER is provided, then that username is used when pushing over ssh to
Gerrit.
This tool must be run as a Gerrit and GitHub administrator. It will temporarily
add you to the list of Git Superusers in order to do the import, and it will
remove you from that group upon completion.
In order to give the tool access to GitHub, you must place a GitHub API token
in ~/.github-api-token. (If for some reason you need to skip updating the GitHub
repository's settings, use -A to skip that step.)
EOF
exit 2
}
function main
{
trap igp_cleanup exit
local SKIP_GH=
while getopts ":Ah" opt; do
case $opt in
A) SKIP_GH=true;;
h) usage;;
?) fail "invalid option: -$OPTARG";;
esac
done
shift $((OPTIND-1))
if [[ $# != 2 && $# != 3 ]]; then
usage
fi
if [[ ! $1 =~ ^[-a-zA-Z0-9_]+$ ]] ||
[[ ! $2 =~ ^[-a-zA-Z0-9_]+$ ]]; then
fail "GitHub account or repo name contains unsupported" \
"characters"
fi
igp_gh_account=$1
igp_gh_repo=$2
igp_gerrit_project="$1/$2"
igp_clonedir="$igp_tmproot/$igp_gh_repo"
if [[ -n "$3" ]]; then
igp_gerrit_sshhost="$3@$igp_gerrit_host"
else
igp_gerrit_sshhost="$igp_gerrit_host"
fi
if [[ -z $SKIP_GH ]]; then
echo "Setting up Github repo for Gerrit pushes ... "
igp_github_protect $igp_gh_account $igp_gh_repo
fi
echo -n "Detecting your Gerrit username ... "
igp_gerrit_configure || fail "failed"
echo "$igp_gerrit_username"
echo "Cloning github.com repository $igp_gerrit_project into " \
"\"$igp_clonedir\"" >&2
mkdir "$igp_tmproot" || fail "failed to mkdir \"$igp_tmproot\""
igp_github_clone $igp_gh_account $igp_gh_repo "$igp_clonedir" ||
fail "failed to clone"
echo "Creating remote Gerrit project $igp_gerrit_project" >&2
igp_project_create $igp_gerrit_project \
"http://github.com/$igp_gh_account/$igp_gh_repo" ||
fail "failed to create Gerrit project"
#
# Fortunately, adding and removing members using this interface is
# idempotent, so we don't need to check first.
#
echo -n "Adding '$igp_gerrit_username' to" \
"group \"$igp_gerrit_group\" ... "
igp_gerrit set-members "'$igp_gerrit_group'" \
--add "$igp_gerrit_username" || fail "failed"
echo "Pushing existing changes to Gerrit project" >&2
igp_gerrit_push "$igp_clonedir" "$igp_gerrit_project" || \
fail "failed to push changes"
echo -n "Removing '$igp_gerrit_username' from" \
"group \"$igp_gerrit_group\" ... "
igp_gerrit set-members "'$igp_gerrit_group'" \
--remove "$igp_gerrit_username" || fail "failed"
cat <<EOF
Success! The following project has been created on the Gerrit server:
https://$igp_gerrit_host/#/admin/projects/$igp_gerrit_project
To make sure replication to GitHub will work, please make sure that the
"Joyent Engineering" team on GitHub has write access to the GitHub project
by visiting this page:
https://github.com/$igp_gerrit_project/settings/collaboration
EOF
}
function fail
{
echo "crimport: $@" >&2
exit 1
}
#
# igp_cleanup: clean up temporary files. We do this for both normal and
# abnormal exit.
#
function igp_cleanup
{
#
# This is just an especially paranoid check so that we don't do
# something terrible.
#
if [[ -d $igp_tmproot ]]; then
echo -n "cleaning up $igp_tmproot ... " >&2
(cd $igp_tmproot && \
[[ -d $igp_gh_repo/.git ]] &&
rm -rf $igp_gh_repo)
rmdir $igp_tmproot
echo "done." >&2
fi
}
#
# igp_gerrit ARGS...
#
# This runs the gerrit CLI over ssh. For example, to run the "flush-caches"
# subcommand, run "igp_gerrit flush-caches".
#
function igp_gerrit
{
ssh $igp_gerrit_sshhost gerrit "$@"
}
#
# igp_project_create NAME SOURCE
#
# Creates a project called NAME on the Gerrit server with appropriate settings.
#
function igp_project_create
{
#
# The extra quotes around the description are necessary, per the Gerrit
# create-project documentation.
#
igp_gerrit "create-project" \
--parent="GitHub-Joyent" \
--description="'Authoritative source for $2'" \
--submit-type=FAST_FORWARD_ONLY \
"$1"
}
#
# igp_github_clone ACCOUNT REPONAME DSTPATH
#
# Clones the specified repository to the specified local path.
#
function igp_github_clone
{
if [[ ! $1 =~ ^[-a-zA-Z0-9_]+$ ]] ||
[[ ! $2 =~ ^[-a-zA-Z0-9_]+$ ]]; then
fail "bogus account or repo name!"
fi
git clone git@github.com:$1/$2.git $3
}
#
# igp_github_protect ACCOUNT REPONAME
#
# Adds branch protection to the Github repo to ensure only Gerrit can
# push to the "master" branch.
#
function igp_github_protect
{
if [[ ! $1 =~ ^[-a-zA-Z0-9_]+$ ]] ||
[[ ! $2 =~ ^[-a-zA-Z0-9_]+$ ]]; then
fail "bogus account or repo name!"
fi
if [[ ! -f $igp_github_token_file ]]; then
fail "couldn't find $igp_github_token_file"
fi
local TOKEN="$(cat $igp_github_token_file)"
local BRANCH="master"
local UNIQUE="$RANDOM$RANDOM$RANDOM:"
# Check that the Gerrit user is able to push
local PERM_RESULT
PERM_RESULT=$(curl -s --write-out "$UNIQUE%{http_code}\n" \
-u "$TOKEN:x-oauth-basic" -X GET \
"$igp_github_api/repos/$1/$2/collaborators/$igp_github_gerrit_user/permission")
if [[ $? != 0 ]]; then
fail "couldn't get permissions for $igp_github_gerrit_user"
fi
local PERM_INFO=$(grep -v "^$UNIQUE" <<< "$PERM_RESULT")
local STATUS=$(grep "^$UNIQUE" <<< "$PERM_RESULT" | cut -d: -f2)
if [[ $STATUS != "200" ]]; then
fail "couldn't get permissions (HTTP status code $STATUS)"
fi
local PERM=$(json permission <<< "$PERM_INFO")
if [[ $PERM != "write" && $PERM != "admin" ]]; then
fail "$igp_github_gerrit_user needs push access to $1/$2"
fi
# Set branch protection on master, so that only Gerrit can push.
curl -fsS -u "$TOKEN:x-oauth-basic" -X PUT -d@- \
"$igp_github_api/repos/$1/$2/branches/$BRANCH/protection" \
> /dev/null <<-EOF
{
"required_status_checks": {
"strict": true,
"contexts": [ ]
},
"required_pull_request_reviews": null,
"enforce_admins": true,
"restrictions": {
"users": [
"$igp_github_gerrit_user"
],
"teams": []
}
}
EOF
if [[ $? != 0 ]]; then
fail "failed to set branch protection"
fi
#
# Disable all but squash merging, to make it harder to accidentally do
# merge commits.
#
# Github doesn't allow completely disabling merge buttons, but we can
# at least restrict it enough to make it harder for anyone to do. In
# the future we could also create a bot that automatically marks all
# PRs as failing checks.
#
curl -fsS -u "$TOKEN:x-oauth-basic" -X PATCH -d@- \
"$igp_github_api/repos/$1/$2" > /dev/null <<-EOF
{
"name": "$2",
"allow_squash_merge": true,
"allow_merge_commit": false,
"allow_rebase_merge": false,
"has_projects": false
}
EOF
if [[ $? != 0 ]]; then
fail "failed to set branch protection"
fi
# Get the "Joyent Engineering" team's ID.
local TEAMS
TEAMS=$(curl -fsS -u "$TOKEN:x-oauth-basic" \
"$igp_github_api/orgs/$1/teams")
if [[ $? != 0 ]]; then
fail "failed to list $1 teams"
fi
local JOYENT_ID=$(json -a \
-c "this.slug === '$igp_github_eng_slug'" id <<< "$TEAMS")
if [[ -z $JOYENT_ID ]]; then
fail "failed to find Joyent Engineering team's ID"
fi
curl -fsS -u "$TOKEN:x-oauth-basic" -X PUT -d@- \
"$igp_github_api/teams/$JOYENT_ID/repos/$1/$2" <<-EOF
{
"permission": "push"
}
EOF
if [[ $? != 0 ]]; then
fail "failed to grant Engineering push access to $1/$2"
fi
}
#
# igp_gerrit_push REPO_PATH PROJECT_NAME
#
# For the local git repository at REPO_PATH, add a remote for the Gerrit project
# PROJECT_NAME and push the repository to it.
#
function igp_gerrit_push
{
(
set -o errexit
cd "$1" 2>&1;
git remote add cr git+ssh://$igp_gerrit_sshhost/$2.git
git push cr master
) || fail "failed to push to Gerrit"
}
#
# igp_gerrit_configure: determine the current Gerrit username based on what the
# server reports when we try to use it. If the user specified a username
# argument, this is redundant. But they may not have, and this approach will
# always report whatever username we're using to access the server.
#
# The actual approach leaves something to be desired, but Gerrit doesn't provide
# the equivalent of who(1) or id(1). On the plus side, this output should be
# stable enough for use here.
#
function igp_gerrit_configure
{
igp_gerrit_username=$(ssh $igp_gerrit_sshhost 2>&1 | \
grep '^ *git clone.*REPOSITORY_NAME.git' | \
sed -e s'#.*ssh://\(.*\)@.*#\1#') ||
fail "failed to parse out Gerrit username"
if [[ -z "$igp_gerrit_username" ]]; then
fail "failed to discover Gerrit username"
fi
}
main "$@"