herdsman 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f18f584c60859f1587dd80aaa46f302cab09efb6
4
+ data.tar.gz: e8a287c600b82bea7fed32924ab6779b3557e77c
5
+ SHA512:
6
+ metadata.gz: 4413b1e186657cd9dbc41be92e6530b509e022fdf1bc1cb604c022a58dcec676acca1f7f67db6ffccf0edfc4f5b43988670d5bfb12787b8cf136b4d5fbf912d2
7
+ data.tar.gz: 80d372ceb0e04bf9baf7775bdf404a6708b7c8ea4bc894ecc0d26e5e4f8ba8b894ea9fa988a9811806441f7e22ec66fce982171031026468ed7d8c69c8661432
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Herdsman
2
+
3
+ Herdsman is a command line utility for working with multiple Git repositories.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ gem install herdsman
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ Commands:
15
+ herdsman help [COMMAND] # Describe available commands or one specific command
16
+ herdsman status # Check the status of the repositories in the herd
17
+ herdsman version # Show the herdsman version
18
+
19
+ Options:
20
+ q, [--quiet], [--no-quiet]
21
+ ```
22
+
23
+ ### `herdsman status`
24
+
25
+ `herdsman status` will check each of the repositories in the herd:
26
+ * for untracked files
27
+ * for modified files
28
+ * for unpushed commits
29
+ * for unpulled commits
30
+ * is on the specified revision (defaults to `master`)
31
+
32
+ ```
33
+ $ herdsman status
34
+ WARN: foo-repo has untracked files
35
+ WARN: foo-repo has modified files
36
+ INFO: bar-repo is ok
37
+ WARN: baz-repo has unpushed commits
38
+ WARN: baz-repo has unpulled commits
39
+ WARN: baz-repo revision is not 'qux-branch'
40
+ ```
41
+
42
+ Exits `0` if all checks pass. Exits `1` if any of the repositories in the herd fail any of the checks.
43
+
44
+ ## Configuration
45
+
46
+ Configure the herd of repositories to manage with a `herdsman.yml` file in the current working directory.
47
+
48
+ Example:
49
+
50
+ ```yml
51
+ repos:
52
+ - /path/to/foo-repo
53
+ - ../../path/to/bar-repo
54
+ - path: /path/to/baz-repo
55
+ revision: qux-branch # defaults to `master` if unset
56
+ ```
57
+
58
+ ## License
59
+
60
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
61
+
62
+ ## Credits
63
+
64
+ * [Thoughtbot's gitsh](https://github.com/thoughtbot/gitsh) Git shell, which was referenced for much of the Git CLI integration.
data/bin/herdsman ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'herdsman'
5
+ require 'herdsman/cli'
6
+
7
+ Herdsman::CLI.start
data/herdsman.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
3
+ require 'herdsman/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.authors = ['Tom Marshall']
7
+ spec.description =
8
+ 'Herdsman is a CLI utility for working with multiple Git repositories'
9
+ spec.files = %w(herdsman.gemspec) + Dir['*.md',
10
+ 'bin/*',
11
+ 'lib/**/*.rb']
12
+ spec.email = 'tommarshall7@gmail.com'
13
+ spec.executables = ['herdsman']
14
+ spec.homepage = 'https://github.com/tommarshall/herdsman'
15
+ spec.license = 'MIT'
16
+ spec.name = 'herdsman'
17
+ spec.require_paths = ['lib']
18
+ spec.summary =
19
+ 'A CLI utility for working with multiple Git repositories'
20
+ spec.version = Herdsman::VERSION
21
+ end
@@ -0,0 +1,60 @@
1
+ require 'thor'
2
+
3
+ module Herdsman
4
+ class CLI < Thor
5
+ require 'herdsman'
6
+
7
+ class_option :quiet, type: :boolean, aliases: :q
8
+
9
+ default_task :status
10
+
11
+ desc 'status', 'Check the status of the repositories in the herd'
12
+ def status
13
+ cmd = Herdsman::Command::Status.new(herd: herd, logger: logger)
14
+ result = cmd.run
15
+ exit result
16
+ end
17
+
18
+ desc 'version', 'Show the herdsman version'
19
+ map %w(-v --version) => :version
20
+ def version
21
+ puts "Herdsman version #{Herdsman::VERSION}"
22
+ end
23
+
24
+ private
25
+
26
+ def config
27
+ Herdsman::Config.new(File.expand_path('herdsman.yml', Dir.pwd))
28
+ rescue
29
+ $stderr.puts "ERROR: #{$!.message}"
30
+ exit 1
31
+ end
32
+
33
+ def env
34
+ Herdsman::Environment.new
35
+ end
36
+
37
+ def herd
38
+ Herdsman::Herd.new(env, config, herd_members(config.repos))
39
+ end
40
+
41
+ def logger
42
+ writer = Logger.new(STDOUT)
43
+ writer.formatter = proc do |severity, _, _, msg|
44
+ "#{severity.upcase}: #{msg}\n"
45
+ end
46
+ logger = Herdsman::LogAdapter.new(writer)
47
+ logger.adjust_verbosity(quiet: options[:quiet])
48
+ logger
49
+ end
50
+
51
+ def herd_members(repos)
52
+ repos.map do |repo|
53
+ Herdsman::HerdMember.new(
54
+ Herdsman::GitRepo.new(env, repo.path),
55
+ repo.revision,
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ module Herdsman
2
+ module Command
3
+ class Status
4
+ def initialize(args = {})
5
+ @herd = args[:herd]
6
+ @logger = args[:logger]
7
+ end
8
+
9
+ def run
10
+ herd.members.each do |herd_member|
11
+ herd_member.status_report.each do |message|
12
+ logger.send(message.level, message.msg)
13
+ end
14
+ end
15
+ herd.gathered?
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :herd, :logger
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,67 @@
1
+ require 'yaml'
2
+
3
+ module Herdsman
4
+ class Config
5
+ def initialize(path)
6
+ @config = read_config!(path)
7
+ validate!
8
+ end
9
+
10
+ def repos
11
+ config_repos = config['repos'] || []
12
+ config_repos.map do |repo_config|
13
+ RepoConfig.new(repo_config)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :config
20
+
21
+ class RepoConfig
22
+ def initialize(repo_config)
23
+ @repo_config = repo_config
24
+ end
25
+
26
+ def path
27
+ if repo_config.is_a?(Hash)
28
+ repo_config['path']
29
+ else
30
+ repo_config
31
+ end
32
+ end
33
+
34
+ def revision
35
+ if repo_config.is_a?(Hash) && repo_config.include?('revision')
36
+ repo_config['revision']
37
+ else
38
+ default_revision
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :repo_config
45
+
46
+ def default_revision
47
+ 'master'
48
+ end
49
+ end
50
+
51
+ def read_config!(path)
52
+ YAML.load_file(path)
53
+ rescue
54
+ raise 'No config found'
55
+ end
56
+
57
+ def validate!
58
+ raise 'Invalid config' unless config.is_a?(Hash) && repos.is_a?(Array)
59
+ raise 'No repos defined' if repos.empty?
60
+ repos.each do |repo|
61
+ unless File.directory?(repo.path)
62
+ raise "#{repo.path} is not a directory"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,10 @@
1
+ module Herdsman
2
+ class Environment
3
+ DEFAULT_GIT_COMMAND = '/usr/bin/env git'.freeze
4
+
5
+ attr_accessor :git_command
6
+ def initialize
7
+ @git_command = DEFAULT_GIT_COMMAND
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,101 @@
1
+ require 'open3'
2
+
3
+ module Herdsman
4
+ class GitRepo
5
+ attr_reader :path
6
+ def initialize(env, path)
7
+ @env = env
8
+ @path = path
9
+ end
10
+
11
+ def initialized?
12
+ !git_dir.empty? && File.exist?(path + '/' + git_dir)
13
+ end
14
+
15
+ def git_dir
16
+ git_output('rev-parse --git-dir')
17
+ end
18
+
19
+ def current_head
20
+ current_branch_name || current_tag_name || abbreviated_commit_ref
21
+ end
22
+
23
+ def has_untracked_files?
24
+ status.untracked_files.any?
25
+ end
26
+
27
+ def has_modified_files?
28
+ status.modified_files.any?
29
+ end
30
+
31
+ def has_unpushed_commits?
32
+ git_output('log --oneline @{u}..').lines.any?
33
+ end
34
+
35
+ def has_unpulled_commits?
36
+ fetch
37
+ git_output('log --oneline ..@{u}').lines.any?
38
+ end
39
+
40
+ def revision?(revision)
41
+ [current_branch_name, current_tag_name, abbreviated_commit_ref].include?(
42
+ revision,
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :env
49
+
50
+ def current_branch_name
51
+ branch_name = git_output('symbolic-ref HEAD --short')
52
+ branch_name unless branch_name.empty?
53
+ end
54
+
55
+ def current_tag_name
56
+ tag_name = git_output('describe --exact-match --tags HEAD')
57
+ tag_name unless tag_name.empty?
58
+ end
59
+
60
+ def abbreviated_commit_ref
61
+ commit_ref = git_output('rev-parse HEAD')
62
+ commit_ref[0, 7].to_s unless commit_ref.empty?
63
+ end
64
+
65
+ def status
66
+ StatusParser.new(git_output('status --porcelain'))
67
+ end
68
+
69
+ def fetch
70
+ git_output('fetch --all')
71
+ end
72
+
73
+ def git_output(command)
74
+ Dir.chdir(File.expand_path(path, Dir.pwd)) do
75
+ Open3.capture3(git_command(command)).first.chomp
76
+ end
77
+ end
78
+
79
+ def git_command(sub_command)
80
+ "#{env.git_command} #{sub_command}"
81
+ end
82
+
83
+ class StatusParser
84
+ def initialize(status_porcelain)
85
+ @status_porcelain = status_porcelain
86
+ end
87
+
88
+ def untracked_files
89
+ status_porcelain.lines.select { |l| l.start_with?('??') }
90
+ end
91
+
92
+ def modified_files
93
+ status_porcelain.lines.select { |l| l =~ /^ ?[A-Z]/ }
94
+ end
95
+
96
+ private
97
+
98
+ attr_reader :status_porcelain
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,18 @@
1
+ module Herdsman
2
+ class Herd
3
+ attr_reader :members
4
+ def initialize(env, config, members)
5
+ @env = env
6
+ @config = config
7
+ @members = members
8
+ end
9
+
10
+ def gathered?
11
+ members.all?(&:gathered?)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :env
17
+ end
18
+ end
@@ -0,0 +1,85 @@
1
+ module Herdsman
2
+ class HerdMember
3
+ attr_reader :name
4
+ def initialize(repo, revision, args = {})
5
+ @repo = repo
6
+ @revision = revision
7
+ @name = args[:name] || default_name
8
+ end
9
+
10
+ def gathered?
11
+ [repo_initialized?,
12
+ repo_has_zero_unpushed_commits?,
13
+ repo_has_zero_unpulled_commits?,
14
+ repo_has_zero_untracked_files?,
15
+ repo_has_zero_modified_files?,
16
+ repo_on_specified_revision?].all?
17
+ end
18
+
19
+ def status_report
20
+ clear_messages
21
+ messages << Message.new(:info, "#{name} is ok") if gathered?
22
+ messages
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :repo, :revision
28
+
29
+ def default_name
30
+ File.basename(repo.path)
31
+ end
32
+
33
+ Message = Struct.new(:level, :msg)
34
+
35
+ def messages
36
+ @messages ||= []
37
+ end
38
+
39
+ def clear_messages
40
+ @messages = []
41
+ end
42
+
43
+ def repo_initialized?
44
+ unless repo.initialized?
45
+ messages << Message.new(:warn, "#{repo.path} is not a git repo")
46
+ end
47
+ repo.initialized?
48
+ end
49
+
50
+ def repo_has_zero_unpushed_commits?
51
+ if repo.has_unpushed_commits?
52
+ messages << Message.new(:warn, "#{name} has unpushed commits")
53
+ end
54
+ !repo.has_unpushed_commits?
55
+ end
56
+
57
+ def repo_has_zero_unpulled_commits?
58
+ if repo.has_unpulled_commits?
59
+ messages << Message.new(:warn, "#{name} has unpulled commits")
60
+ end
61
+ !repo.has_unpulled_commits?
62
+ end
63
+
64
+ def repo_has_zero_untracked_files?
65
+ if repo.has_untracked_files?
66
+ messages << Message.new(:warn, "#{name} has untracked files")
67
+ end
68
+ !repo.has_untracked_files?
69
+ end
70
+
71
+ def repo_has_zero_modified_files?
72
+ if repo.has_modified_files?
73
+ messages << Message.new(:warn, "#{name} has modified files")
74
+ end
75
+ !repo.has_modified_files?
76
+ end
77
+
78
+ def repo_on_specified_revision?
79
+ if repo.initialized? && !repo.revision?(revision)
80
+ messages << Message.new(:warn, "#{name} revision is not '#{revision}'")
81
+ end
82
+ !repo.initialized? || repo.revision?(revision)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,46 @@
1
+ module Herdsman
2
+ class LogAdapter
3
+ attr_accessor :writer
4
+
5
+ LOG_LEVELS = {
6
+ debug: ::Logger::DEBUG,
7
+ info: ::Logger::INFO,
8
+ warn: ::Logger::WARN,
9
+ error: ::Logger::ERROR,
10
+ }.freeze
11
+
12
+ def log_level
13
+ writer.level
14
+ end
15
+
16
+ def log_level=(level)
17
+ writer.level = LOG_LEVELS.fetch(level)
18
+ end
19
+
20
+ def adjust_verbosity(options = {})
21
+ if options[:quiet]
22
+ self.log_level = :error
23
+ end
24
+ end
25
+
26
+ def initialize(writer)
27
+ @writer = writer
28
+ end
29
+
30
+ def debug(message)
31
+ writer.debug(message)
32
+ end
33
+
34
+ def info(message)
35
+ writer.info(message)
36
+ end
37
+
38
+ def warn(message)
39
+ writer.warn(message)
40
+ end
41
+
42
+ def error(message)
43
+ writer.error(message)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module Herdsman
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/herdsman.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'logger'
2
+ require 'herdsman/command/status'
3
+ require 'herdsman/config'
4
+ require 'herdsman/environment'
5
+ require 'herdsman/git_repo'
6
+ require 'herdsman/herd'
7
+ require 'herdsman/herd_member'
8
+ require 'herdsman/log_adapter'
9
+ require 'herdsman/version'
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: herdsman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Marshall
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Herdsman is a CLI utility for working with multiple Git repositories
14
+ email: tommarshall7@gmail.com
15
+ executables:
16
+ - herdsman
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - bin/herdsman
22
+ - herdsman.gemspec
23
+ - lib/herdsman.rb
24
+ - lib/herdsman/cli.rb
25
+ - lib/herdsman/command/status.rb
26
+ - lib/herdsman/config.rb
27
+ - lib/herdsman/environment.rb
28
+ - lib/herdsman/git_repo.rb
29
+ - lib/herdsman/herd.rb
30
+ - lib/herdsman/herd_member.rb
31
+ - lib/herdsman/log_adapter.rb
32
+ - lib/herdsman/version.rb
33
+ homepage: https://github.com/tommarshall/herdsman
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.4.5.1
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: A CLI utility for working with multiple Git repositories
57
+ test_files: []