teachers_pet 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +7 -0
- data/Guardfile +3 -1
- data/README.md +53 -28
- data/Rakefile +1 -1
- data/bin/teachers_pet +4 -0
- data/lib/teachers_pet.rb +4 -1
- data/lib/teachers_pet/actions/add_to_team.rb +31 -0
- data/lib/teachers_pet/actions/base.rb +36 -111
- data/lib/teachers_pet/actions/clone_repos.rb +9 -36
- data/lib/teachers_pet/actions/create_repos.rb +16 -39
- data/lib/teachers_pet/actions/create_student_teams.rb +35 -0
- data/lib/teachers_pet/actions/fork_collab.rb +4 -19
- data/lib/teachers_pet/actions/open_issue.rb +17 -32
- data/lib/teachers_pet/actions/push_files.rb +8 -22
- data/lib/teachers_pet/cli.rb +22 -0
- data/lib/teachers_pet/client_decorator.rb +59 -0
- data/lib/teachers_pet/commands/add_to_team.rb +12 -0
- data/lib/teachers_pet/commands/clone_repos.rb +15 -0
- data/lib/teachers_pet/commands/create_repos.rb +15 -0
- data/lib/teachers_pet/commands/create_student_teams.rb +13 -0
- data/lib/teachers_pet/commands/fork_collab.rb +12 -0
- data/lib/teachers_pet/commands/open_issue.rb +18 -0
- data/lib/teachers_pet/commands/push_files.rb +15 -0
- data/lib/teachers_pet/configuration.rb +0 -13
- data/lib/teachers_pet/version.rb +1 -1
- data/spec/actions/base_spec.rb +13 -0
- data/spec/cli_spec.rb +17 -0
- data/spec/commands/add_to_team_spec.rb +43 -0
- data/spec/commands/clone_repos_spec.rb +33 -0
- data/spec/commands/create_repos_spec.rb +55 -0
- data/spec/commands/create_student_teams_spec.rb +90 -0
- data/spec/commands/fork_collab_spec.rb +95 -0
- data/spec/commands/open_issue_spec.rb +63 -0
- data/spec/commands/push_files_spec.rb +33 -0
- data/spec/fixtures/empty +0 -0
- data/spec/fixtures/teams +1 -0
- data/spec/spec_helper.rb +13 -53
- data/spec/support/command_helpers.rb +11 -0
- data/spec/support/common_helpers.rb +52 -0
- data/teachers_pet.gemspec +6 -2
- metadata +89 -29
- data/bin/clone_repos +0 -6
- data/bin/create_repos +0 -6
- data/bin/create_teams +0 -6
- data/bin/fork_collab +0 -6
- data/bin/open_issue +0 -6
- data/bin/push_files +0 -6
- data/lib/teachers_pet/actions/create_teams.rb +0 -101
- data/spec/actions/clone_repos_spec.rb +0 -44
- data/spec/actions/create_repos_spec.rb +0 -65
- data/spec/actions/create_teams_spec.rb +0 -40
- data/spec/actions/open_issue_spec.rb +0 -71
@@ -1,45 +1,27 @@
|
|
1
|
-
# Author: Mike Helmick
|
2
|
-
# Script to create assignment repositories for students under the appropriate organization
|
3
|
-
|
4
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
|
5
|
-
|
6
|
-
require 'rubygems'
|
7
|
-
require 'highline/question'
|
8
|
-
require 'highline/import'
|
9
|
-
require 'highline/compatibility'
|
10
|
-
require 'teachers_pet/actions/base'
|
11
|
-
|
12
1
|
module TeachersPet
|
13
2
|
module Actions
|
14
3
|
class CreateRepos < Base
|
15
4
|
def read_info
|
16
|
-
@repository =
|
17
|
-
@organization =
|
18
|
-
@
|
19
|
-
@instructor_file = self.get_instructors_file_path
|
20
|
-
@public_repos = confirm('Create repositories as public?', false)
|
21
|
-
@add_init_files = confirm('Add .gitignore and README.md files? (skip this if you are pushing starter files.)', false)
|
5
|
+
@repository = self.repository
|
6
|
+
@organization = self.organization
|
7
|
+
@public_repos = self.public?
|
22
8
|
end
|
23
9
|
|
24
10
|
def load_files
|
25
|
-
@students =
|
26
|
-
@instructors = read_file(@instructor_file, 'Instructors')
|
11
|
+
@students = self.read_students_file
|
27
12
|
end
|
28
13
|
|
29
14
|
def create
|
30
|
-
pub_private_text = @public_repos ? 'public' : 'private'
|
31
|
-
confirm("Create #{@students.keys.size} #{pub_private_text} repositories for students and give access to instructors?")
|
32
|
-
|
33
15
|
# create a repo for each student
|
34
16
|
self.init_client
|
35
17
|
|
36
|
-
org_hash =
|
18
|
+
org_hash = self.client.organization(@organization)
|
37
19
|
abort('Organization could not be found') if org_hash.nil?
|
38
20
|
puts "Found organization at: #{org_hash[:login]}"
|
39
21
|
|
40
22
|
# Load the teams - there should be one team per student.
|
41
23
|
# Repositories are given permissions by teams
|
42
|
-
org_teams = get_teams_by_name(@organization)
|
24
|
+
org_teams = self.client.get_teams_by_name(@organization)
|
43
25
|
# For each student - create a repository, and give permissions to their "team"
|
44
26
|
# The repository name is teamName-repository
|
45
27
|
puts "\nCreating assignment repositories for students..."
|
@@ -50,26 +32,21 @@ module TeachersPet
|
|
50
32
|
end
|
51
33
|
repo_name = "#{student}-#{@repository}"
|
52
34
|
|
53
|
-
if repository?(@organization, repo_name)
|
35
|
+
if self.client.repository?(@organization, repo_name)
|
54
36
|
puts " --> Already exists, skipping '#{repo_name}'"
|
55
37
|
next
|
56
38
|
end
|
57
39
|
|
58
|
-
git_ignore_template = "C++" ## This is specific to my current class, you'll want to change
|
59
|
-
git_ignore_template = '' unless @add_init_files
|
60
40
|
puts " --> Creating '#{repo_name}' public? #{@public_repos}"
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
:auto_init => @add_init_files,
|
71
|
-
:gitignore_template => git_ignore_template
|
72
|
-
})
|
41
|
+
self.client.create_repository(repo_name,
|
42
|
+
description: "#{@repository} created for #{student}",
|
43
|
+
private: !@public_repos,
|
44
|
+
has_issues: true,
|
45
|
+
has_wiki: false,
|
46
|
+
has_downloads: false,
|
47
|
+
organization: @organization,
|
48
|
+
team_id: org_teams[student][:id]
|
49
|
+
)
|
73
50
|
end
|
74
51
|
end
|
75
52
|
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module TeachersPet
|
2
|
+
module Actions
|
3
|
+
class CreateStudentTeams < Base
|
4
|
+
def create_student_teams
|
5
|
+
teams_by_name = self.client.existing_teams_by_name(self.organization)
|
6
|
+
|
7
|
+
students_list = self.read_students_file
|
8
|
+
students_list.each do |key, value|
|
9
|
+
if value
|
10
|
+
# Create one team per group of students
|
11
|
+
team_name = key
|
12
|
+
usernames = value
|
13
|
+
else
|
14
|
+
# Create a team with the same name as the student, with that person as the only member
|
15
|
+
team_name = key
|
16
|
+
usernames = [value]
|
17
|
+
end
|
18
|
+
|
19
|
+
team = teams_by_name[team_name]
|
20
|
+
if team
|
21
|
+
puts "Team @#{organization}/#{team_name} already exists."
|
22
|
+
else
|
23
|
+
team = self.client.create_team(organization, team_name)
|
24
|
+
end
|
25
|
+
self.client.add_users_to_team(organization, team, usernames)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
self.init_client
|
31
|
+
self.create_student_teams
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,33 +1,19 @@
|
|
1
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'highline/question'
|
5
|
-
require 'highline/import'
|
6
|
-
require 'highline/compatibility'
|
7
|
-
require 'teachers_pet/actions/base'
|
8
|
-
|
9
1
|
module TeachersPet
|
10
2
|
module Actions
|
11
3
|
class ForkCollab < Base
|
12
|
-
def read_info
|
13
|
-
@repository = ask("Which repository? (owner/repo)")
|
14
|
-
end
|
15
|
-
|
16
4
|
def get_forks
|
17
|
-
|
5
|
+
self.client.forks(self.repository)
|
18
6
|
end
|
19
7
|
|
20
8
|
def promote
|
21
9
|
self.init_client
|
22
|
-
|
23
10
|
forks = self.get_forks
|
24
|
-
|
25
|
-
confirm("Are you sure you want to add #{forks.count} users as collaborators on '#{@repository}'?")
|
26
|
-
|
27
11
|
forks.each do |fork|
|
28
12
|
login = fork.owner.login
|
29
13
|
if fork.owner.type == "User"
|
30
|
-
|
14
|
+
unless self.dry_run?
|
15
|
+
result = self.client.add_collab(self.repository, login)
|
16
|
+
end
|
31
17
|
puts "#{login} - #{result}"
|
32
18
|
else
|
33
19
|
puts "#{login} - false (Organization)"
|
@@ -36,7 +22,6 @@ module TeachersPet
|
|
36
22
|
end
|
37
23
|
|
38
24
|
def run
|
39
|
-
self.read_info
|
40
25
|
self.promote
|
41
26
|
end
|
42
27
|
end
|
@@ -1,49 +1,34 @@
|
|
1
|
-
# Opens a single issue in each repository. The body of the issue is loaded
|
2
|
-
# from a file and the user is prompted for the title.
|
3
|
-
|
4
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
|
5
|
-
|
6
|
-
require 'rubygems'
|
7
|
-
require 'highline/question'
|
8
|
-
require 'highline/import'
|
9
|
-
require 'teachers_pet/actions/base'
|
10
|
-
|
11
1
|
module TeachersPet
|
12
2
|
module Actions
|
13
3
|
class OpenIssue < Base
|
14
4
|
def read_info
|
15
|
-
@repository =
|
16
|
-
@organization =
|
17
|
-
|
18
|
-
@issue = {
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@issue[:options] = options
|
26
|
-
|
27
|
-
@student_file = self.get_students_file_path
|
28
|
-
@instructor_file = self.get_instructors_file_path
|
5
|
+
@repository = self.repository
|
6
|
+
@organization = self.organization
|
7
|
+
|
8
|
+
@issue = {
|
9
|
+
title: self.title,
|
10
|
+
options: {
|
11
|
+
labels: self.labels
|
12
|
+
}
|
13
|
+
}
|
14
|
+
@issue_file = self.body
|
29
15
|
end
|
30
16
|
|
31
17
|
def load_files
|
32
|
-
@students =
|
33
|
-
@instructors = read_file(@instructor_file, 'Instructors')
|
18
|
+
@students = self.read_students_file
|
34
19
|
@issue[:body] = File.open(@issue_file).read
|
35
20
|
end
|
36
21
|
|
37
22
|
def create
|
38
|
-
confirm("Create issue '#{@issue[:title]}' in #{@students.keys.size} student repositories - '#{@repository}'?")
|
23
|
+
# confirm("Create issue '#{@issue[:title]}' in #{@students.keys.size} student repositories - '#{@repository}'?")
|
39
24
|
self.init_client
|
40
25
|
|
41
|
-
org_hash =
|
26
|
+
org_hash = self.client.organization(@organization)
|
42
27
|
abort('Organization could not be found') if org_hash.nil?
|
43
28
|
puts "Found organization at: #{org_hash[:login]}"
|
44
29
|
|
45
|
-
org_teams = get_teams_by_name(@organization)
|
46
|
-
|
30
|
+
org_teams = self.client.get_teams_by_name(@organization)
|
31
|
+
|
47
32
|
puts "\nCreating issue in repositories..."
|
48
33
|
@students.keys.sort.each do |student|
|
49
34
|
unless org_teams.key?(student)
|
@@ -52,13 +37,13 @@ module TeachersPet
|
|
52
37
|
end
|
53
38
|
repo_name = "#{student}-#{@repository}"
|
54
39
|
|
55
|
-
unless repository?(@organization, repo_name)
|
40
|
+
unless self.client.repository?(@organization, repo_name)
|
56
41
|
puts " --> Repository not found, skipping '#{repo_name}'"
|
57
42
|
next
|
58
43
|
end
|
59
44
|
|
60
45
|
# Create the issue with octokit
|
61
|
-
|
46
|
+
self.client.create_issue("#{@organization}/#{repo_name}", @issue[:title], @issue[:body], @issue[:options])
|
62
47
|
end
|
63
48
|
end
|
64
49
|
|
@@ -1,40 +1,26 @@
|
|
1
|
-
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'highline/question'
|
5
|
-
require 'highline/import'
|
6
|
-
require 'highline/compatibility'
|
7
|
-
require 'teachers_pet/actions/base'
|
8
|
-
|
9
|
-
# This script should be run within a working directory that is a git repository.
|
10
|
-
# It will add a remote that is the name of each student team to your repository
|
11
|
-
|
12
1
|
module TeachersPet
|
13
2
|
module Actions
|
14
3
|
class PushFiles < Base
|
15
4
|
def read_info
|
16
|
-
@repository =
|
17
|
-
|
18
|
-
@
|
19
|
-
@student_file = self.get_students_file_path
|
20
|
-
@sshEndpoint = ask('What is the ssh endpoint?') { |q| q.default = TeachersPet::Configuration.sshEndpoint }
|
5
|
+
@repository = self.repository
|
6
|
+
@organization = self.organization
|
7
|
+
@sshEndpoint = self.ssh
|
21
8
|
end
|
22
9
|
|
23
10
|
def load_files
|
24
|
-
@students =
|
11
|
+
@students = self.read_students_file
|
25
12
|
end
|
26
13
|
|
27
14
|
def push
|
28
|
-
confirm("Push files to student repositories?")
|
29
15
|
self.init_client
|
30
16
|
|
31
|
-
org_hash =
|
17
|
+
org_hash = self.client.organization(@organization)
|
32
18
|
abort('Organization could not be found') if org_hash.nil?
|
33
19
|
puts "Found organization at: #{org_hash[:url]}"
|
34
20
|
|
35
21
|
# Load the teams - there should be one team per student.
|
36
22
|
# Repositories are given permissions by teams
|
37
|
-
org_teams = get_teams_by_name(@organization)
|
23
|
+
org_teams = self.client.get_teams_by_name(@organization)
|
38
24
|
|
39
25
|
# For each student - if an appropraite repository exists,
|
40
26
|
# add it to the list.
|
@@ -46,13 +32,13 @@ module TeachersPet
|
|
46
32
|
end
|
47
33
|
repo_name = "#{student}-#{@repository}"
|
48
34
|
|
49
|
-
unless repository?(@organization, repo_name)
|
35
|
+
unless self.client.repository?(@organization, repo_name)
|
50
36
|
puts(" ** ERROR ** - no repository called #{repo_name}")
|
51
37
|
end
|
52
38
|
if TeachersPet::Configuration.remoteSsh
|
53
39
|
remotes_to_add[student] = "git@#{@sshEndpoint}:#{@organization}/#{repo_name}.git"
|
54
40
|
else
|
55
|
-
remotes_to_add[student] = "#{
|
41
|
+
remotes_to_add[student] = "#{self.web}#{@organization}/#{repo_name}.git"
|
56
42
|
end
|
57
43
|
end
|
58
44
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module TeachersPet
|
4
|
+
class Cli < Thor
|
5
|
+
# TODO figure out a way to display options as groups
|
6
|
+
|
7
|
+
def self.common_options
|
8
|
+
check_unknown_options!
|
9
|
+
|
10
|
+
option :username, default: ENV['USER']
|
11
|
+
option :password
|
12
|
+
option :token, default: ENV['TEACHERS_PET_GITHUB_TOKEN'], desc: "Provide a token instead of a username+password to authenticate via OAuth. See https://github.com/education/teachers_pet#authentication."
|
13
|
+
|
14
|
+
option :api, banner: 'ORIGIN', default: Configuration.apiEndpoint, desc: "The API endpoint of your GitHub Enterprise instance, if you have one."
|
15
|
+
option :web, banner: 'ORIGIN', default: Configuration.webEndpoint, desc: "The URL of your GitHub Enterprise instance, if you have one."
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.students_option
|
19
|
+
option :students, default: TeachersPet::Configuration.studentsFile, banner: 'PATH', desc: "The path to the file containing the list of students"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module TeachersPet
|
2
|
+
class ClientDecorator < SimpleDelegator
|
3
|
+
def repository?(organization, repo_name)
|
4
|
+
begin
|
5
|
+
self.repository("#{organization}/#{repo_name}")
|
6
|
+
rescue
|
7
|
+
return false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_teams_by_name(organization)
|
12
|
+
org_teams = self.organization_teams(organization)
|
13
|
+
teams = Hash.new
|
14
|
+
org_teams.each do |team|
|
15
|
+
teams[team[:name]] = team
|
16
|
+
end
|
17
|
+
return teams
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_team_member_logins(team_id)
|
21
|
+
self.team_members(team_id).map do |member|
|
22
|
+
member[:login]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def existing_teams_by_name(organization)
|
27
|
+
results = Hash.new
|
28
|
+
teams = self.organization_teams(organization)
|
29
|
+
teams.each do |team|
|
30
|
+
results[team[:name]] = team
|
31
|
+
end
|
32
|
+
|
33
|
+
results
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_team(organization, name)
|
37
|
+
puts "Creating team @#{organization}/#{name} ..."
|
38
|
+
super(organization,
|
39
|
+
name: name,
|
40
|
+
permission: 'push'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_users_to_team(organization, team, usernames)
|
45
|
+
# Minor optimization, mostly for testing
|
46
|
+
if usernames.any?
|
47
|
+
team_members = self.get_team_member_logins(team[:id])
|
48
|
+
usernames.each do |username|
|
49
|
+
if team_members.include?(username)
|
50
|
+
puts " -> @#{username} is already on @#{organization}/#{team[:name]}"
|
51
|
+
else
|
52
|
+
self.add_team_member(team[:id], username)
|
53
|
+
puts " -> @#{username} has been added to @#{organization}/#{team[:name]}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module TeachersPet
|
2
|
+
class Cli
|
3
|
+
option :organization, required: true
|
4
|
+
option :members, required: true, banner: 'PATH', desc: "The path to the file containing the list of members to add. The filename will be used as the name of the team, e.g. `path/to/instructors.csv` will use the 'instructors' team."
|
5
|
+
common_options
|
6
|
+
|
7
|
+
desc 'add_to_team', "Adds each user in the list to the team specified my the filename. Creates the team on the specified organization if it doesn't already exist."
|
8
|
+
def add_to_team
|
9
|
+
TeachersPet::Actions::AddToTeam.new(options).run
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module TeachersPet
|
2
|
+
class Cli
|
3
|
+
option :organization, required: true
|
4
|
+
option :repository, required: true
|
5
|
+
option :clone_method, default: 'https', desc: "'https' or 'ssh'"
|
6
|
+
|
7
|
+
students_option
|
8
|
+
common_options
|
9
|
+
|
10
|
+
desc 'clone_repos', "Clone all student repositories for a particular assignment into the current directory."
|
11
|
+
def clone_repos
|
12
|
+
TeachersPet::Actions::CloneRepos.new(options).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module TeachersPet
|
2
|
+
class Cli
|
3
|
+
option :organization, required: true
|
4
|
+
option :repository, required: true
|
5
|
+
option :public, type: :boolean, default: false, desc: "Make the repositories public"
|
6
|
+
|
7
|
+
students_option
|
8
|
+
common_options
|
9
|
+
|
10
|
+
desc 'create_repos', "Create assignment repositories for students."
|
11
|
+
def create_repos
|
12
|
+
TeachersPet::Actions::CreateRepos.new(options).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|