-
Notifications
You must be signed in to change notification settings - Fork 0
/
ch
executable file
·360 lines (307 loc) · 11.8 KB
/
ch
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
#!/bin/bash
# bash options
set -e # exit on error
set -u # exit on undefined variable (use ${varname:-} to check if a variable is defined)
# Class-Hub CLI
# This script is used to manage students repositories on github using the github api
# Check if the gh cli is installed
[[ ! -x "$(command -v gh)" ]] && { echo "GitHub CLI is not installed. Please install it from https://cli.github.com/"; exit 1; }
# Check if the jq cli is installed
[[ ! -x "$(command -v jq)" ]] && { echo "jq is not installed. Please install it from https://stedolan.github.io/jq/"; exit 1; }
# Check if the gh cli is authenticated
if ! gh auth status > /dev/null 2>&1; then
echo "GitHub CLI is not authenticated. Please run 'gh auth login' to authenticate."
exit 1
fi
# Add this helper function to calculate lective year
function get_lective_year() {
local current_month=$(date +%m)
local current_year=$(date +%Y)
# If we're between January and July, we're in the second half of the academic year
# So we use the previous year as the start
if [ "$current_month" -le 7 ]; then
echo "$(($current_year % 100 - 1))$(($current_year % 100))"
else
# If we're between August and December, we're in the first half
echo "$(($current_year % 100))$(($current_year % 100 + 1))"
fi
}
# Add this helper function to check if a GitHub user exists
function github_user_exists() {
local email="$1"
# Search for user by email using the GitHub API
if gh api "search/users?q=${email}+in:email" | jq -e '.total_count > 0' >/dev/null; then
return 0
else
return 1
fi
}
# Create a new assignment
# An assignment consists of a repository template and a list of students organized in groups
function create_assignment() {
# Check required arguments
if [ "$#" -lt 3 ]; then
echo "Usage: ch create-assignment <classroom_name> <assignment_name> <students_file> [template_repo]"
exit 1
fi
local classroom_name="$1"
local assignment_name="$2"
local students_file="$3"
local template_repo="${4:-}" # Optional template repository
echo "Creating assignment '$assignment_name' in classroom '$classroom_name'..."
# Validate that students file exists
if [ ! -f "$students_file" ]; then
echo "Error: Students file '$students_file' not found"
exit 1
fi
# Create associative array to store students by group
declare -A group_students
# Get current lective year
local lective_year=$(get_lective_year)
# First pass: collect all students by group
while IFS=, read -r email group_number || [ -n "$email" ]; do
# Skip empty lines and comments
[[ -z "$email" || "$email" =~ ^#.*$ ]] && continue
# Trim whitespace
email=$(echo "$email" | xargs)
group_number=$(echo "$group_number" | xargs)
# Append student email to group array
if [ -z "${group_students[$group_number]:-}" ]; then
group_students[$group_number]="$email"
else
group_students[$group_number]="${group_students[$group_number]} $email"
fi
done < "$students_file"
# Show preview of repositories and students
echo -e "\nThe following repositories will be created:"
# Create a sorted array of group numbers
readarray -t sorted_groups < <(printf '%s\n' "${!group_students[@]}" | sort -n)
# Display groups in sorted order
for group_number in "${sorted_groups[@]}"; do
repo_name="${lective_year}-${assignment_name}-group$(printf "%02d" "$group_number")"
echo -e "\n Repository: $classroom_name/$repo_name"
echo " Students:"
for email in ${group_students[$group_number]}; do
username=$(gh api "search/users?q=${email}+in:email" | jq -r '.items[0].login // "not found"')
if [ "$username" = "not found" ]; then
echo " - $email (⚠️ GitHub user not found)"
else
echo " - $email (@$username)"
fi
done
done
# Ask for confirmation
echo -e "\nPlease review the repository names and student assignments."
read -p "Do you want to proceed? (y/N) " -n 1 -r
echo # Move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Operation cancelled."
exit 1
fi
# Second pass: create repository for each group and add all students
for group_number in "${sorted_groups[@]}"; do
# Create repository name starting with lective year, using printf to pad group number with zeros
repo_name="${lective_year}-${assignment_name}-group$(printf "%02d" "$group_number")"
# Check if repository already exists
if gh repo view "$classroom_name/$repo_name" >/dev/null 2>&1; then
echo "Repository $repo_name already exists, skipping..."
continue
fi
# Create repository
echo "Creating repository $repo_name..."
if [ -n "$template_repo" ]; then
gh repo create "$classroom_name/$repo_name" --private --template "$template_repo"
else
gh repo create "$classroom_name/$repo_name" --private
fi
# Add all students in the group as collaborators
for email in ${group_students[$group_number]}; do
if github_user_exists "$email"; then
# Get the username from the email
username=$(gh api "search/users?q=${email}+in:email" | jq -r '.items[0].login')
echo " - Inviting @$username ($email) to $repo_name..."
gh api -X PUT "repos/$classroom_name/$repo_name/collaborators/$username" -f permission=write >/dev/null 2>&1
else
echo " ⚠️ Warning: GitHub user '$email' not found, skipping..."
fi
done
done
echo -e "\nAssignment creation completed!"
}
function list_assignments() {
# Check required arguments
if [ "$#" -lt 1 ]; then
echo "Usage: ch list-assignments <classroom_name> [assignment_name]"
exit 1
fi
local classroom_name="$1"
local assignment_name="${2:-}" # Optional assignment name filter
echo "Listing assignments in classroom '$classroom_name'..."
if [ -n "$assignment_name" ]; then
# If assignment name is provided, filter repositories that start with that name
gh repo list "$classroom_name" --json name --jq ".[] | select(.name | startswith(\"$assignment_name\")) | .name"
else
# List all repositories in the classroom
gh repo list "$classroom_name" --json name --jq '.[].name'
fi
}
function get_assignment() {
# Check required arguments
if [ "$#" -lt 2 ]; then
echo "Usage: ch get-assignment <classroom_name> <assignment_name> [target_directory]"
exit 1
fi
local classroom_name="$1"
local assignment_name="$2"
local target_dir="${3:-.}" # Optional target directory, defaults to current directory
echo "Getting assignment '$assignment_name' in classroom '$classroom_name'..."
# List all repositories for this assignment
repos=$(gh repo list "$classroom_name" --json name --jq ".[] | select(.name | startswith(\"$assignment_name\")) | .name")
# Show repositories that will be processed and ask for confirmation
echo -e "\nThe following repositories will be updated:"
echo "$repos" | sed 's/^/ - /'
read -p "Do you want to proceed? (y/N) " -n 1 -r
echo # Move to a new line
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Operation cancelled."
exit 1
fi
# Create target directory if it doesn't exist
mkdir -p "$target_dir"
cd "$target_dir"
# Process each repository
for repo in $repos; do
if [ -d "$repo" ]; then
echo "Repository $repo exists, pulling latest changes..."
(cd "$repo" && git pull)
else
echo "Cloning repository $repo..."
gh repo clone "$classroom_name/$repo"
fi
done
echo "Assignment repositories updated successfully!"
}
function update() {
echo "Checking for updates..."
# Define the repository and script location
local repo="eupedrosa/class-hub"
local script_path="ch"
local current_script="$0"
# Get the latest version from GitHub
local tmp_file=$(mktemp)
if ! gh api "repos/$repo/contents/$script_path" --jq '.content' | base64 -d > "$tmp_file"; then
echo "Error: Failed to fetch the latest version"
rm "$tmp_file"
exit 1
fi
# Compare files
if cmp -s "$current_script" "$tmp_file"; then
echo "Already up to date!"
rm "$tmp_file"
exit 0
fi
# Replace current script with new version
mv "$tmp_file" "$current_script"
chmod +x "$current_script"
# Update autocomplete file
local autocomplete_path="$HOME/.local/share/bash-completion/completions/ch"
if [ -f "$autocomplete_path" ]; then
echo "Updating autocomplete file..."
mkdir -p "$(dirname "$autocomplete_path")"
"$current_script" autocomplete > "$autocomplete_path"
fi
echo "Successfully updated!"
}
function autocomplete() {
cat << 'EOF'
_ch_completion() {
local cur prev words cword
_get_comp_words_by_ref -n : cur prev words cword
# List of available commands
local commands="create-assignment list-assignments get-assignment update"
# Handle command-specific completions
case ${words[1]} in
create-assignment)
case $cword in
2) # classroom_name - complete with organization names
local orgs=$(gh api user/memberships/orgs --jq '.[].organization.login')
COMPREPLY=($(compgen -W "$orgs" -- "$cur"))
;;
4) # students_file - complete with .csv files
COMPREPLY=($(compgen -f -X '!*.csv' -- "$cur"))
;;
5) # template_repo - complete with repository names
local repos=$(gh repo list --json nameWithOwner --jq '.[].nameWithOwner')
COMPREPLY=($(compgen -W "$repos" -- "$cur"))
;;
esac
;;
list-assignments|get-assignment)
case $cword in
2) # classroom_name - complete with organization names
local orgs=$(gh api user/memberships/orgs --jq '.[].organization.login')
COMPREPLY=($(compgen -W "$orgs" -- "$cur"))
;;
esac
;;
*)
# Complete command names
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
;;
esac
return 0
}
# Register the completion function
complete -F _ch_completion ch
EOF
}
# Handle arguments
while [[ $# -gt 0 ]]; do
case $1 in
create-assignment)
shift
create_assignment "$@"
exit 0
;;
list-assignments)
shift
list_assignments "$@"
exit 0
;;
get-assignment)
shift
get_assignment "$@"
exit 0
;;
update)
shift
update "$@"
exit 0
;;
autocomplete)
shift
autocomplete "$@"
exit 0
;;
-h|--help)
echo "Usage: $0 <command> <options>"
echo ""
echo "Commands:"
echo " create-assignment <classroom_name> <assignment_name> <students_file> [template_repo]"
echo " list-assignments <classroom_name> [assignment_name]"
echo " get-assignment <classroom_name> <assignment_name> [target_directory]"
echo " update Update the CLI tool to the latest version"
echo " autocomplete Output shell completion code"
exit 0
;;
*)
echo "Error: Unknown command '$1'"
echo "Run '$0 --help' for usage information"
exit 1
;;
esac
done
# If no command is provided, show help
echo "Error: No command provided"
echo "Run '$0 --help' for usage information"
exit 1