gitomator 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +57 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +194 -0
  8. data/Rakefile +9 -0
  9. data/bin/gitomator-clone-repos +21 -0
  10. data/bin/gitomator-console +19 -0
  11. data/bin/gitomator-disable-ci +24 -0
  12. data/bin/gitomator-enable-ci +24 -0
  13. data/bin/gitomator-make-access-permissions +72 -0
  14. data/bin/gitomator-make-repos +45 -0
  15. data/bin/gitomator-make-teams +20 -0
  16. data/bin/setup +8 -0
  17. data/gitomator.gemspec +28 -0
  18. data/lib/gitomator/console.rb +54 -0
  19. data/lib/gitomator/context.rb +132 -0
  20. data/lib/gitomator/exceptions.rb +21 -0
  21. data/lib/gitomator/service/ci.rb +35 -0
  22. data/lib/gitomator/service/git.rb +48 -0
  23. data/lib/gitomator/service/hosting.rb +204 -0
  24. data/lib/gitomator/service/tagging.rb +99 -0
  25. data/lib/gitomator/service.rb +64 -0
  26. data/lib/gitomator/service_provider/git_shell.rb +68 -0
  27. data/lib/gitomator/service_provider/hosting_local.rb +103 -0
  28. data/lib/gitomator/task/base_repos_task.rb +88 -0
  29. data/lib/gitomator/task/clone_repos.rb +61 -0
  30. data/lib/gitomator/task/config/repos_config.rb +117 -0
  31. data/lib/gitomator/task/config/team_config.rb +57 -0
  32. data/lib/gitomator/task/enable_disable_ci.rb +56 -0
  33. data/lib/gitomator/task/make_repos.rb +86 -0
  34. data/lib/gitomator/task/setup_team.rb +63 -0
  35. data/lib/gitomator/task/update_repo_access_permissions.rb +42 -0
  36. data/lib/gitomator/task.rb +48 -0
  37. data/lib/gitomator/util/repo/name_resolver.rb +68 -0
  38. data/lib/gitomator/util/script_util.rb +69 -0
  39. data/lib/gitomator/version.rb +3 -0
  40. data/lib/gitomator.rb +61 -0
  41. metadata +173 -0
@@ -0,0 +1,103 @@
1
+ require 'fileutils'
2
+
3
+
4
+ module Gitomator
5
+ module ServiceProvider
6
+ # A hosting provider that manages repos in a directory on the local
7
+ # file-system.
8
+ class HostingLocal
9
+
10
+
11
+ #-------------------------------------------------------------------------
12
+
13
+ #
14
+ # A small wrapper that takes a hash, and create an attr_accessor for
15
+ # each hash key.
16
+ # This is a temporary implementation, until we create proper model
17
+ # objects (e.g. HostedRepo, Team, PullRequest, etc.)
18
+ #
19
+ class ModelObject
20
+ def initialize(hash)
21
+ hash.each do |key, value|
22
+ setter = "#{key}="
23
+ self.class.send(:attr_accessor, key) if !respond_to?(setter)
24
+ send setter, value
25
+ end
26
+ end
27
+ end
28
+
29
+ #-------------------------------------------------------------------------
30
+
31
+
32
+ attr_reader :local_dir, :local_repos_dir
33
+
34
+ def initialize(git_service, local_dir, opts = {})
35
+ @git = git_service
36
+
37
+ raise "Local directory doesn't exist, #{local_dir}" unless Dir.exist? local_dir
38
+ @local_dir = local_dir
39
+
40
+ @local_repos_dir = File.join(@local_dir, opts[:repos_dir] || 'repos')
41
+ Dir.mkdir @local_repos_dir unless Dir.exist? @local_repos_dir
42
+ end
43
+
44
+ #---------------------------------------------------------------------
45
+
46
+ def name
47
+ :local
48
+ end
49
+
50
+
51
+ def repo_root(name)
52
+ File.join(local_repos_dir, name)
53
+ end
54
+
55
+ #---------------------------------------------------------------------
56
+
57
+ def create_repo(name, opts)
58
+ raise "Directory exists, #{repo_root(name)}" if Dir.exist? repo_root(name)
59
+ @git.init(repo_root(name), opts)
60
+ return ModelObject.new({
61
+ :name => name, :full_name => name, :url => "#{repo_root(name)}/.git"
62
+ })
63
+ end
64
+
65
+
66
+ def read_repo(name)
67
+ if Dir.exist? repo_root(name)
68
+ return ModelObject.new({
69
+ :name => name, :full_name => name, :url => "#{repo_root(name)}/.git"
70
+ })
71
+ else
72
+ return nil
73
+ end
74
+ end
75
+
76
+
77
+ def update_repo(name, opts={})
78
+ if opts[:name]
79
+ _rename_repo(name, opts[:name])
80
+ end
81
+ return read_repo(name)
82
+ end
83
+
84
+
85
+ def delete_repo(name)
86
+ if Dir.exist? repo_root(name)
87
+ FileUtils.rm_rf repo_root(name)
88
+ else
89
+ raise "No such repo, '#{name}'"
90
+ end
91
+ return nil
92
+ end
93
+
94
+
95
+ def _rename_repo(old_name, new_name)
96
+ raise "No such repo '#{old_name}'" unless Dir.exist? repo_root(old_name)
97
+ FileUtils.mv repo_root(old_name), repo_root(new_name)
98
+ end
99
+
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,88 @@
1
+ require 'gitomator/task'
2
+
3
+ module Gitomator
4
+ module Task
5
+ class BaseReposTask < Gitomator::BaseTask
6
+
7
+ attr_reader :local_dir # String or nil
8
+ attr_reader :repos # Array<String>
9
+
10
+ #
11
+ # @param context [Gitomator::Context]
12
+ # @param auto_marker_config [Gitomator::Classroom::Config::AutoMarker] Parsed configuration object (TODO: Implement it as a subclass of Gitomator::Classroom::Assignment)
13
+ # @param local_dir [String] A local directory where the repos will be (or have been) cloned.
14
+ #
15
+ def initialize(context, repos, local_dir=nil)
16
+ super(context)
17
+ unless local_dir.nil?
18
+ raise "No such directory #{local_dir}" unless Dir.exists? local_dir
19
+ end
20
+ @local_dir = local_dir
21
+ @repos = repos
22
+ @blocks = { :before => [], :after => [] }
23
+ end
24
+
25
+
26
+
27
+ def run()
28
+ @blocks[:before].each {|b| self.instance_exec(&b) }
29
+
30
+ repo2result, repo2error = {}, {}
31
+
32
+ repos.each_with_index do |repo, index|
33
+ logger.debug "#{repo} (#{index + 1} out of #{repos.length})"
34
+ begin
35
+ repo2result[repo] = process_repo(repo, index)
36
+ rescue => e
37
+ process_repo_error(repo, index, e)
38
+ repo2error[repo] = e
39
+ end
40
+ end
41
+
42
+ @blocks[:after].each {|b| self.instance_exec(repo2result, repo2error, &b) }
43
+ end
44
+
45
+
46
+
47
+ #
48
+ # You need to override this method!
49
+ #
50
+ # @param repo [String] The name of the repo
51
+ # @return Object (Optionally) return a result that can be used after processing all repos
52
+ #
53
+ def process_repo(repo, index)
54
+ raise "Unimplemented"
55
+ end
56
+
57
+ #
58
+ # Override this to provide custom error handling
59
+ #
60
+ def process_repo_error(repo, index, error)
61
+ logger.error "#{repo} : #{error}\n#{error.backtrace.join("\n\t")}"
62
+ end
63
+
64
+
65
+ #
66
+ # Inject a block that will run before processing any repos.
67
+ # The blocks takes no arguments, and doesn't (need to) return any specific value.
68
+ #
69
+ def before_processing_any_repos(&block)
70
+ @blocks[:before].push block
71
+ end
72
+
73
+ #
74
+ # Inject a block that will run after all repos have been processed.
75
+ #
76
+ # @yield [result2mark, repo2error]
77
+ # @yieldparam [Hash<String,Object>] repo2result
78
+ # @yieldparam [Hash<String,Error>] repo2error
79
+ #
80
+ def after_processing_all_repos(&block)
81
+ @blocks[:after].push block
82
+ end
83
+
84
+
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,61 @@
1
+ require 'gitomator/task/base_repos_task'
2
+
3
+ module Gitomator
4
+ module Task
5
+ class CloneRepos < Gitomator::Task::BaseReposTask
6
+
7
+
8
+ #
9
+ # @param context [Gitomator::Context]
10
+ # @param repos [Array<String>] The repos to clone
11
+ # @param local_dir [String] A local directory where the repos will be cloned.
12
+ # @param opts [Hash]
13
+ #
14
+ def initialize(context, repos, local_dir, opts={})
15
+ super(context, repos, local_dir)
16
+ @opts = opts
17
+
18
+ before_processing_any_repos do
19
+ logger.debug "Clonning #{repos.length} repo(s) into #{local_dir} ..."
20
+ end
21
+
22
+ after_processing_all_repos do |repo2result, repo2error|
23
+ cloned = repo2result.select {|_, result| result}.length
24
+ skipped = repo2result.reject {|_, result| result}.length
25
+ errored = repo2error.length
26
+ logger.info "Done (#{cloned} cloned, #{skipped} skipped, #{errored} errors)"
27
+ end
28
+ end
29
+
30
+
31
+ # override
32
+ def process_repo(source, index)
33
+ namespace = hosting.resolve_namespace(source)
34
+ repo_name = hosting.resolve_repo_name(source)
35
+ branch = hosting.resolve_branch(source)
36
+
37
+ local_repo_root = File.join(@local_dir, repo_name)
38
+ if Dir.exist? local_repo_root
39
+ logger.info "Local clone exists, #{local_repo_root}"
40
+ return false
41
+ end
42
+
43
+ repo = hosting.read_repo(repo_name)
44
+ raise "No such remote repo, #{repo_name}" if repo.nil?
45
+
46
+ logger.info "git clone #{repo.url}"
47
+ git.clone(repo.url, local_repo_root)
48
+
49
+ unless branch.nil?
50
+ logger.debug("Switching to remote branch #{branch}")
51
+ git.checkout(local_repo_root, branch, {:is_new => true, :is_remote => true})
52
+ end
53
+
54
+ return true
55
+ end
56
+
57
+
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,117 @@
1
+
2
+ module Gitomator
3
+ module Task
4
+ module Config
5
+
6
+
7
+ class ReposConfig
8
+
9
+ attr_reader :default_access_permission
10
+ attr_reader :repo_properties
11
+ attr_reader :source_repo
12
+
13
+ #
14
+ # @param config_obj [Hash] Configuration data (commonly loaded from a YAML file)
15
+ #
16
+ def initialize(config_obj)
17
+ raise "Missing required key, repos" unless config_obj.has_key? 'repos'
18
+
19
+ @default_access_permission = (config_obj['default_access_permission'] || :read).to_sym
20
+ @source_repo = config_obj['source_repo']
21
+ @repo_properties = config_obj['repo_properties'] || {}
22
+
23
+ @repo2permissions = parse_repo2permissions(config_obj['repos'])
24
+ end
25
+
26
+
27
+ #
28
+ # @return [Enumerable<Strings>] The names of the repos.
29
+ #
30
+ def repos
31
+ @repo2permissions.keys
32
+ end
33
+
34
+ #
35
+ # @param repo [String] The name of a repository
36
+ # @return [Hash<String,Symbol>] Map name (user or team) to permission (:read/:write/:admin)
37
+ #
38
+ def permissions(repo)
39
+ @repo2permissions[repo]
40
+ end
41
+
42
+
43
+ # =================== Protected Helper Methods =========================
44
+
45
+ protected
46
+
47
+
48
+ #
49
+ # Helper function.
50
+ # @param repos_config [Array] Array of repo config items (various formats are supported)
51
+ #
52
+ def parse_repo2permissions(repos_config)
53
+ repo2permissions = {}
54
+
55
+ repos_config.each do |repo_config|
56
+ repo_name = nil
57
+ permissions = {}
58
+
59
+ # 1. Parse it ...
60
+ if repo_config.is_a? String
61
+ repo_name = repo_config
62
+ elsif (repo_config.is_a? Hash) and (repo_config.length == 1)
63
+ repo_name = repo_config.keys.first.to_s
64
+ permissions = parse_permissions(repo_config[repo_name])
65
+ else
66
+ raise Gitomator::Classroom::Exception::InvalidConfig.new(
67
+ "Cannot parse #{repo_config} (expected String or a Hash with one entry)")
68
+ end
69
+
70
+ # 2. Check if there was an error ...
71
+ if repo2permissions.has_key? repo_config
72
+ raise Gitomator::Classroom::Exception::InvalidConfig.new("Duplicate property, #{repo_config}")
73
+ end
74
+
75
+ # 3. If no error, store the information
76
+ repo2permissions[repo_name] = permissions
77
+ end
78
+
79
+ return repo2permissions
80
+ end
81
+
82
+
83
+ #
84
+ # Parse the permissions configuration of a single repo into a hash that
85
+ # maps user/team names (Strings) to access-permissions (Symbols).
86
+ #
87
+ # Various configuration formats are supported:
88
+ # * String - Single name, with default permission
89
+ # * Hash - Maps names to permissions
90
+ # * Array<String/Hash> - An array mixing both of the previous options.
91
+ #
92
+ # @param permissions_config [String/Hash/Array]
93
+ # @return [Hash<String,Symbol>]
94
+ #
95
+ def parse_permissions(permissions_config)
96
+ if permissions_config.is_a? String
97
+ return { permissions_config => default_access_permission }
98
+
99
+ elsif permissions_config.is_a? Hash
100
+ return permissions_config.map {|name,perm| [name, perm.to_sym] } .to_h
101
+
102
+ elsif permissions_config.is_a? Array
103
+ return permissions_config.map {|x| parse_permissions(x) } .reduce(:merge)
104
+
105
+ else
106
+ raise "Invalid permission configuration, #{permissions_config}"
107
+ end
108
+ end
109
+
110
+
111
+
112
+ end
113
+
114
+
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,57 @@
1
+ module Gitomator
2
+ module Task
3
+ module Config
4
+
5
+ class TeamConfig
6
+
7
+
8
+ #=========================================================================
9
+ # Static factory methods
10
+
11
+ #
12
+ # @param config [Hash] - Configuration data (e.g. parsed from a YAML file)
13
+ # @return [Enumerable<Gitomator::Task::Config::TeamConfig>]
14
+ #
15
+ def self.from_hash(config)
16
+ return config.map {|name, members| new(name, members) }
17
+ end
18
+
19
+ #=========================================================================
20
+
21
+
22
+ attr_reader :name, :members # Hash[String -> String], username to role
23
+
24
+ #
25
+ # @param name [String]
26
+ # @param members_config [Array] Each item is either a string (username) or Hash with one entry (username -> role)
27
+ #
28
+ def initialize(name, members_config)
29
+ @name = name
30
+ @members = parse_members_config(members_config)
31
+ end
32
+
33
+
34
+ def parse_members_config(members_config)
35
+ result = {}
36
+
37
+ members_config.each do |entry|
38
+ if entry.is_a? String
39
+ result[entry] = 'member' # Default role is 'member'
40
+ elsif entry.is_a?(Hash) && entry.length == 1
41
+ result[entry.keys.first] = entry.values.first
42
+ else
43
+ raise "Invalid team-member config, #{entry}."
44
+ end
45
+ end
46
+
47
+ return result
48
+
49
+ end
50
+
51
+
52
+ end
53
+
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ require 'gitomator/task/base_repos_task'
2
+
3
+ module Gitomator
4
+ module Task
5
+
6
+ #
7
+ # Abstract parent class
8
+ #
9
+ class EnableDisableCI < Gitomator::Task::BaseReposTask
10
+
11
+ #
12
+ # @param context - Has a `ci` method that returns a Gitomator::Service::CI
13
+ # @param repos [Array<String>] - Names of the repos to enable/disable CI on.
14
+ # @param opts [Hash<Symbol,Object>] - Task options
15
+ # @option opts [Boolean] :sync - Indicate whether we should start by sync'ing the CI service.
16
+ #
17
+ def initialize(context, repos, opts={})
18
+ super(context, repos)
19
+
20
+ if opts[:sync]
21
+ before_processing_any_repos do
22
+ logger.info "Syncing CI service (this may take a little while) ..."
23
+ ci.sync()
24
+ while ci.syncing?
25
+ print "."
26
+ sleep 1
27
+ end
28
+ puts ""
29
+ logger.info "CI service synchronized"
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ end
36
+
37
+
38
+
39
+ class EnableCI < EnableDisableCI
40
+ def process_repo(repo_name, i)
41
+ logger.info "Enabling CI for #{repo_name} (#{i + 1} out of #{repos.length})"
42
+ ci.enable_ci repo_name
43
+ end
44
+ end
45
+
46
+
47
+ class DisableCI < EnableDisableCI
48
+ def process_repo(repo_name, i)
49
+ logger.info "Disabling CI for #{repo_name} (#{i + 1} out of #{repos.length})"
50
+ ci.disable_ci repo_name
51
+ end
52
+ end
53
+
54
+
55
+ end
56
+ end
@@ -0,0 +1,86 @@
1
+ require 'tmpdir'
2
+ require 'gitomator/task/base_repos_task'
3
+ require 'gitomator/task/clone_repos'
4
+
5
+ module Gitomator
6
+ module Task
7
+ class MakeRepos < Gitomator::Task::BaseReposTask
8
+
9
+
10
+ attr_reader :source_repo
11
+ attr_reader :update_existing
12
+ attr_reader :repo_properties
13
+ attr_reader :source_repo_local_root
14
+
15
+ #
16
+ # @param context
17
+ # @param repos [Array<String>]
18
+ # @param opts [Hash<Symbol,Object>]
19
+ # @option opts [Hash<Symbol,Object>] :repo_properties - For example, :private, :description, :has_issues, etc.
20
+ # @option opts [String] :source_repo - The name of a repo that will be the "starting point" of all the created repos.
21
+ # @option opts [Boolean] :update_existing - Update existing repos, by pushing latest commit(s) from the source_repo.
22
+ #
23
+ def initialize(context, repos, opts={})
24
+ super(context, repos)
25
+ @opts = opts
26
+
27
+ @source_repo = opts[:source_repo]
28
+ @update_existing = opts[:update_existing] || false
29
+ @repo_properties = opts[:repo_properties] || {}
30
+ @repo_properties = @repo_properties.map {|k,v| [k.to_sym,v] }.to_h
31
+
32
+ before_processing_any_repos do
33
+ logger.info "About to create/update #{repos.length} repo(s) ..."
34
+
35
+ if source_repo
36
+ tmp_dir = Dir.mktmpdir('Gitomator_')
37
+ Gitomator::Task::CloneRepos.new(context, [source_repo], tmp_dir).run()
38
+ repo_name = hosting.resolve_repo_name(source_repo)
39
+ @source_repo_local_root = File.join(tmp_dir, repo_name)
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+
46
+ def process_repo(repo_name, index)
47
+ repo = hosting.read_repo(repo_name)
48
+
49
+ # If the repo doesn't exist, create it ...
50
+ if repo.nil?
51
+ logger.debug "Creating new repo #{repo_name} ..."
52
+ repo = hosting.create_repo(repo_name, repo_properties)
53
+ push_commits(repo, source_repo_local_root)
54
+
55
+ # If the repo exists, we might need to push changes, or update its properties
56
+ else
57
+ if update_existing
58
+ push_commits(repo, source_repo_local_root)
59
+ end
60
+ update_properties_if_needed(repo, repo_properties)
61
+ end
62
+ end
63
+
64
+
65
+ def push_commits(hosted_repo, local_repo)
66
+ if local_repo
67
+ logger.debug "Pushing commits from #{local_repo} to #{hosted_repo.name} "
68
+ git.set_remote(local_repo, hosted_repo.name, hosted_repo.url, {create: true})
69
+ git.command(local_repo, "push #{hosted_repo.name} HEAD:master")
70
+ end
71
+ end
72
+
73
+
74
+ def update_properties_if_needed(repo, props)
75
+ p = repo.properties
76
+ diff = props.select {|k,v| p.has_key?(k) && p[k] != v}
77
+ unless(diff.empty?)
78
+ logger.debug "Updating #{repo.name} properties #{diff}"
79
+ hosting.update_repo(repo.name, diff)
80
+ end
81
+ end
82
+
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,63 @@
1
+ require 'gitomator/task'
2
+ require 'set'
3
+
4
+ module Gitomator
5
+ module Task
6
+
7
+ class SetupTeam < Gitomator::BaseTask
8
+
9
+ #
10
+ # @param context
11
+ # @param team_name [String]
12
+ # @param member2role [Hash<String,String>] Map usernames to roles.
13
+ # @param opts [Hash]
14
+ #
15
+ def initialize(context, team_name, member2role, opts={})
16
+ super(context)
17
+ @team_name = team_name
18
+ @member2role = member2role
19
+ @opts = opts
20
+ end
21
+
22
+
23
+
24
+ def run
25
+ create_team_if_missing()
26
+ @member2role.each do |username, role|
27
+ begin
28
+ create_or_update_membership(username, role)
29
+ rescue => e
30
+ logger.error("Cannot create/update #{username}'s membership in " +
31
+ "#{@team_name} - #{e}.\n#{e.backtrace.join("\n\t")}")
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ def create_team_if_missing
38
+ if hosting.read_team(@team_name).nil?
39
+ logger.info("Creating team: #{@team_name}")
40
+ hosting.create_team(@team_name)
41
+ else
42
+ logger.debug("Team #{@team_name} exists.")
43
+ end
44
+ end
45
+
46
+
47
+ def create_or_update_membership(username, role)
48
+ current_role = hosting.read_team_membership(@team_name, username)
49
+ if current_role.nil?
50
+ logger.info("Adding #{username} to team #{@team_name} (role: #{role}).")
51
+ hosting.create_team_membership(@team_name, username, role)
52
+ elsif current_role != role
53
+ logger.info("Updating #{username}'s role from #{current_role} to #{role} (team: #{@team_name})")
54
+ hosting.update_team_membership(@team_name, username, role)
55
+ else
56
+ logger.debug("Skipping #{username}, already a #{role} of #{@team_name}.")
57
+ end
58
+ end
59
+
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ require 'gitomator/task'
2
+ require 'set'
3
+
4
+ module Gitomator
5
+ module Task
6
+
7
+ class UpdateRepoAccessPermissions < Gitomator::BaseTask
8
+
9
+ #
10
+ # @param context
11
+ # @param repo_name [String]
12
+ # @param user2perm [Hash<String,Symbol>] Map usernames to permission type (:read/:write).
13
+ # @param team2perm [Hash<String,Symbol>] Map team-names to permission type (:read/:write).
14
+ # @param opts [Hash]
15
+ #
16
+ def initialize(context, repo_name, user2perm, team2perm, opts={})
17
+ super(context)
18
+ @repo_name = repo_name
19
+ @user2perm = user2perm || {}
20
+ @team2perm = team2perm || {}
21
+ @opts = opts
22
+ end
23
+
24
+
25
+
26
+ def run
27
+ @user2perm.each do |username, permission|
28
+ logger.info("Granting user #{username} #{permission} permission to #{@repo_name}")
29
+ hosting.set_user_permission(username, @repo_name, permission)
30
+ end
31
+
32
+ @team2perm.each do |team_name, permission|
33
+ logger.info("Granting team #{team_name} #{permission} permission to #{@repo_name}")
34
+ hosting.set_team_permission(team_name, @repo_name, permission)
35
+ end
36
+ end
37
+
38
+
39
+
40
+ end
41
+ end
42
+ end