qb 0.1.36 → 0.1.37

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ba8c588f4c5401fbefe1a8000ea8354707269fe6
4
- data.tar.gz: 5fcac34c7a16b53e92b5db9190edf215cadd8884
3
+ metadata.gz: cafbcb81c0a8179b326824b5b89203534604398c
4
+ data.tar.gz: 46823f3b27926e4c5652f4fac6b2932d3d74250a
5
5
  SHA512:
6
- metadata.gz: 76ca3d2c460501ed7ece33233a38f33e9f0fd70eaee54e1f0cff8c5339b348806d4afd010099b7d933409210c0e54d8dbfb08770ee24b7a7767f608da013533c
7
- data.tar.gz: cb7bd2957dab55aaabbb49bd996cc3e8b7425ec0f3688c5f5bfe0c7c8c0c8221d047e29b027ebf7c00c4899c5a593f8323f066e112856d144bde970f9867a809
6
+ metadata.gz: b6d08dab932b56a417c31fd56dd3584d8cf6874f6f467dcd22e575806fe39e8894fa447905f18258023be19a0f2c0b17389d1bdb941397058b63f567180fdb8c
7
+ data.tar.gz: 3167ccdd9f411767e2302c58adf8d697e428c7515d5c30502e645fe4c43a2d707d34e99fe2c77850c45e77b0607f33dff6bde49744de9ea855ecc48cea16daa0
@@ -1,5 +1,5 @@
1
1
  [defaults]
2
2
 
3
- # roles_path = ./tmp/roles:./roles
3
+ roles_path = ./dev/scratch
4
4
  filter_plugins = ./roles/qb.gem/filter_plugins
5
5
  retry_files_enabled = False
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bundle exec rake
@@ -0,0 +1,2 @@
1
+ ---
2
+ # defaults file for ansible_module
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # WANT_JSON
3
+
4
+ # init bundler in dev env
5
+ if ENV['QB_DEV_ENV']
6
+ ENV.each {|k, v|
7
+ if k.start_with? 'QB_DEV_ENV_'
8
+ ENV[k.sub('QB_DEV_ENV_', '')] = v
9
+ end
10
+ }
11
+ require 'bundler/setup'
12
+ end
13
+
14
+ require 'qb'
15
+
16
+ class Test < QB::AnsibleModule
17
+ def main
18
+ changed! blah: "blow me: #{ @args['x'] }"
19
+ end
20
+ end
21
+
22
+ Test.new.run
@@ -0,0 +1,8 @@
1
+ ---
2
+ # meta file for ansible_module
3
+
4
+ allow_duplicates: yes
5
+
6
+ dependencies: []
7
+ # - role: role-name
8
+
@@ -0,0 +1,44 @@
1
+ ---
2
+ # meta/qb.yml file for ansible_module
3
+ #
4
+ # qb settings for this role. see README.md for more info.
5
+ #
6
+
7
+ # description of the role to show in it's help output.
8
+ description: null
9
+
10
+ # prefix for role variables
11
+ var_prefix: null
12
+
13
+ # how to get a default for `dir` if it's not provided as the only
14
+ # positional argument. if a positional argument is provided it will
15
+ # override the method defined here.
16
+ #
17
+ # options:
18
+ #
19
+ # - null
20
+ # - require the value on the command line.
21
+ # - git_root
22
+ # - use the git root fof the directory that the `qb` command is invoked
23
+ # from. useful for 'project-centric' commands so they can be invoked
24
+ # from anywhere in the repo.
25
+ # - cwd
26
+ # - use the directory the `qb` command is invoked form.
27
+ # - {exe: PATH}
28
+ # - invoke an execuable, passing a JSON serialization of the options
29
+ # mapping their CLI names to values. path can be relative to role
30
+ # directory.
31
+ default_dir: cwd
32
+
33
+ # default user to become for play
34
+ default_user: null
35
+
36
+ # set to false to not save options in .qb-options.yml files
37
+ save_options: false
38
+
39
+ options: []
40
+ # - name: example
41
+ # description: an example of a variable.
42
+ # required: false
43
+ # type: boolean # boolean (default) | string
44
+ # short: e
@@ -0,0 +1,9 @@
1
+ ---
2
+ # tasks file for ansible_module
3
+
4
+ - name: test QB::AnsibleModule
5
+ test:
6
+ x: ex
7
+
8
+ - debug:
9
+ msg: "{{ blah }}"
@@ -0,0 +1,4 @@
1
+ ---
2
+ stdio_raise: false
3
+ stdio_count: false
4
+
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # WANT_JSON
3
+
4
+ # init bundler in dev env
5
+ if ENV['QB_DEV_ENV']
6
+ ENV.each {|k, v|
7
+ if k.start_with? 'QB_DEV_ENV_'
8
+ ENV[k.sub('QB_DEV_ENV_', '')] = v
9
+ end
10
+ }
11
+ require 'bundler/setup'
12
+ end
13
+
14
+ require 'qb'
15
+ require 'pp'
16
+
17
+ class Test < QB::AnsibleModule
18
+ def main
19
+ QB.debug args: @args
20
+
21
+ if @args['count']
22
+ (1..10).each {|i|
23
+ puts i
24
+ sleep 1
25
+ }
26
+ end
27
+
28
+ raise "HERE" if @args['raise']
29
+
30
+ nil
31
+ end
32
+ end
33
+
34
+ Test.new.run
@@ -0,0 +1,8 @@
1
+ ---
2
+ # meta file for stdio
3
+
4
+ allow_duplicates: yes
5
+
6
+ dependencies: []
7
+ # - role: role-name
8
+
@@ -0,0 +1,55 @@
1
+ ---
2
+ # meta/qb.yml file for stdio
3
+ #
4
+ # qb settings for this role. see README.md for more info.
5
+ #
6
+
7
+ # description of the role to show in it's help output.
8
+ description: null
9
+
10
+ # prefix for role variables
11
+ var_prefix: null
12
+
13
+ # how to get a default for `dir` if it's not provided as the only
14
+ # positional argument. if a positional argument is provided it will
15
+ # override the method defined here.
16
+ #
17
+ # options:
18
+ #
19
+ # - null
20
+ # - require the value on the command line.
21
+ # - git_root
22
+ # - use the git root fof the directory that the `qb` command is invoked
23
+ # from. useful for 'project-centric' commands so they can be invoked
24
+ # from anywhere in the repo.
25
+ # - cwd
26
+ # - use the directory the `qb` command is invoked form.
27
+ # - {exe: PATH}
28
+ # - invoke an execuable, passing a JSON serialization of the options
29
+ # mapping their CLI names to values. path can be relative to role
30
+ # directory.
31
+ default_dir: cwd
32
+
33
+ # default user to become for play
34
+ default_user: null
35
+
36
+ # set to false to not save options in .qb-options.yml files
37
+ save_options: false
38
+
39
+ options:
40
+ # - name: example
41
+ # description: an example of a variable.
42
+ # required: false
43
+ # type: boolean # boolean (default) | string
44
+ # short: e
45
+ - name: raise
46
+ description: raise an error in main.
47
+ required: false
48
+ type: boolean
49
+ short: r
50
+
51
+ - name: count
52
+ description: count to 10 on stdout with 1 sec sleeps between
53
+ required: false
54
+ type: boolean
55
+ short: c
@@ -0,0 +1,5 @@
1
+ ---
2
+ # tasks file for stdio
3
+ - test:
4
+ count: "{{ stdio_count }}"
5
+ raise: "{{ stdio_raise }}"
data/exe/qb CHANGED
@@ -42,7 +42,7 @@ end
42
42
 
43
43
  def set_debug! args
44
44
  if DEBUG_ARGS.any? {|arg| args.include? arg}
45
- QB.debug = true
45
+ ENV['QB_DEBUG'] = 'true'
46
46
  debug "ON"
47
47
  DEBUG_ARGS.each {|arg| args.delete arg}
48
48
  end
@@ -78,11 +78,15 @@ def with_clean_env &block
78
78
  ].include?(k)
79
79
  }
80
80
 
81
+ qb_env = ENV.select {|k, v| k.start_with? 'QB_'}
82
+
81
83
  Bundler.with_clean_env do
82
84
  # now that we're in a clean env, copy the Bundler env vars into
83
85
  # 'QB_DEV_ENV_<NAME>' vars.
84
86
  dev_env.each {|k, v| ENV["QB_DEV_ENV_#{ k }"] = v}
85
87
 
88
+ qb_env.each {|k, v| ENV[k] = v}
89
+
86
90
  # and set QB_DEV_ENV=true
87
91
  ENV['QB_DEV_ENV'] = 'true'
88
92
 
@@ -352,8 +356,17 @@ def main args
352
356
 
353
357
  puts "COMMAND: #{ cmd }"
354
358
 
359
+ # boot up stdio services so that ansible modules can stream to our
360
+ # stdout and stderr to print stuff (including debug lines) in real-time
361
+ stdio_services = {'out' => $stdout, 'err' => $stderr}.map do |name, dest|
362
+ QB::Util::STDIO::Service.new(name, dest).tap {|s| s.open! }
363
+ end
364
+
355
365
  status = Cmds.stream cmd
356
366
 
367
+ # close the stdio services
368
+ stdio_services.each {|s| s.close! }
369
+
357
370
  if status != 0
358
371
  puts "ERROR ansible-playbook failed."
359
372
  end
data/lib/qb.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'nrser/extras'
2
2
 
3
3
  require "qb/version"
4
+ require "qb/util"
5
+ require 'qb/util/stdio'
6
+ require "qb/ansible_module"
4
7
 
5
8
  module QB
6
9
  ROOT = (Pathname.new(__FILE__).dirname + '..').expand_path
@@ -10,16 +13,8 @@ module QB
10
13
  class Error < StandardError
11
14
  end
12
15
 
13
- # TODO this should be in an instance that is run instead of module global
14
- # hack for now
15
- @@debug = false
16
-
17
- def self.debug= bool
18
- @@debug = !!bool
19
- end
20
-
21
16
  def self.debug *args
22
- return unless @@debug && args.length > 0
17
+ return unless ENV['QB_DEBUG'] && args.length > 0
23
18
 
24
19
  header = 'DEBUG'
25
20
 
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+
3
+ module QB
4
+ class AnsibleModule
5
+ def self.stringify_keys hash
6
+ hash.map {|k, v| [k.to_s, v]}.to_h
7
+ end
8
+
9
+ def initialize
10
+ @changed = false
11
+ @input_file = ARGV[0]
12
+ @input = File.read @input_file
13
+ @args = JSON.load @input
14
+ @facts = {}
15
+
16
+ # if QB_STDIO_ env vars are set send stdout and stderr
17
+ # to those sockets to print in the parent process
18
+
19
+ if ENV['QB_STDIO_OUT']
20
+ $stdout = UNIXSocket.new ENV['QB_STDIO_OUT']
21
+ end
22
+
23
+ if ENV['QB_STDIO_ERR']
24
+ $stderr = UNIXSocket.new ENV['QB_STDIO_ERR']
25
+ end
26
+ end
27
+
28
+ def run
29
+ result = main
30
+
31
+ case result
32
+ when nil
33
+ # pass
34
+ when Hash
35
+ @facts.merge! result
36
+ else
37
+ raise "result of #main should be nil or Hash, found #{ result.inspect }"
38
+ end
39
+
40
+ done
41
+ end
42
+
43
+ def changed! facts = {}
44
+ @changed = true
45
+ @facts.merge! facts
46
+ done
47
+ end
48
+
49
+ def done
50
+ exit_json changed: @changed,
51
+ ansible_facts: self.class.stringify_keys(@facts)
52
+ end
53
+
54
+ def exit_json hash
55
+ # print JSON response to process' actual STDOUT (instead of $stdout,
56
+ # which may be pointing to the qb parent process)
57
+ STDOUT.print JSON.dump(self.class.stringify_keys(hash))
58
+ exit 0
59
+ end
60
+
61
+ def fail msg
62
+ exit_json failed: true, msg: msg
63
+ end
64
+ end
65
+ end # QB
@@ -161,7 +161,7 @@ module QB
161
161
 
162
162
  qb_options = {
163
163
  'hosts' => ['localhost'],
164
- 'facts' => false,
164
+ 'facts' => true,
165
165
  }
166
166
 
167
167
  if role.meta['default_user']
@@ -212,11 +212,10 @@ module QB
212
212
  end
213
213
 
214
214
  opts.on(
215
- '-F',
216
- '--FACTS',
217
- "gather facts (often un-needed)",
215
+ '--NO-FACTS',
216
+ "don't gather facts",
218
217
  ) do |value|
219
- qb_options['facts'] = value
218
+ qb_options['facts'] = false
220
219
  end
221
220
 
222
221
  add opts, role_options, role
@@ -1,5 +1,6 @@
1
1
  require 'yaml'
2
2
  require 'cmds'
3
+ require 'parseconfig'
3
4
 
4
5
  module QB
5
6
  class Role
@@ -46,17 +47,44 @@ module QB
46
47
  ['qb.yml', 'qb'].any? {|filename| pathname.join('meta', filename).file?}
47
48
  end
48
49
 
49
- # array of Pathname places to look for role dirs.
50
- def self.search_path
51
- [
52
- QB::ROLES_DIR,
53
- Pathname.new(Dir.getwd).join('roles'),
54
- Pathname.new(Dir.getwd).join('ansible', 'roles'),
55
- Pathname.new(Dir.getwd).join('dev', 'roles'),
56
- Pathname.new(Dir.getwd).join('dev', 'roles', 'tmp'),
50
+ # get role paths from ansible.cfg if it exists in a directory.
51
+ #
52
+ # @param dir [Pathname] directory to look for ansible.cfg in.
53
+ #
54
+ # @return [Array<String>] role paths
55
+ #
56
+ def self.cfg_roles_path dir
57
+ path = dir.join 'ansible.cfg'
58
+
59
+ if path.file?
60
+ config = ParseConfig.new path.to_s
61
+ config['defaults']['roles_path'].split(':').map {|path|
62
+ QB::Util.resolve dir, path
63
+ }
64
+ else
65
+ []
66
+ end
67
+ end
68
+
69
+ # @param dir [Pathname] dir to include.
70
+ def self.roles_paths dir
71
+ cfg_roles_path(dir) + [
72
+ dir.join('roles'),
73
+ dir.join('roles', 'tmp')
57
74
  ]
58
75
  end
59
76
 
77
+ # @return [Array<Pathname>] places to look for role dirs.
78
+ def self.search_path
79
+ [QB::ROLES_DIR] + [
80
+ QB::Util.resolve,
81
+ QB::Util.resolve('ansible'),
82
+ QB::Util.resolve('dev'),
83
+ ].map {|dir|
84
+ roles_paths dir
85
+ }.flatten
86
+ end
87
+
60
88
  # array of QB::Role found in search path.
61
89
  def self.available
62
90
  search_path.
@@ -69,6 +97,8 @@ module QB
69
97
  search_dir.children.select {|child| role_dir? child }
70
98
  }.
71
99
  flatten.
100
+ # should allow uniq to remove dups
101
+ map {|role_dir| role_dir.realpath }.
72
102
  # needed when qb is run from the qb repo since QB::ROLES_DIR and
73
103
  # ./roles are the same dir
74
104
  uniq.
@@ -79,8 +109,10 @@ module QB
79
109
 
80
110
  # get an array of QB::Role that match an input string
81
111
  def self.matches input
112
+ available = self.available
113
+
82
114
  available.each {|role|
83
- # exact match to relitive path
115
+ # exact match to relative path
84
116
  return [role] if role.rel_path.to_s == input
85
117
  }.each {|role|
86
118
  # exact match to full name
@@ -88,12 +120,29 @@ module QB
88
120
  }.each {|role|
89
121
  # exact match without the namespace prefix ('qb.' or similar)
90
122
  return [role] if role.namespaceless == input
91
- }.select {|role|
92
- # select any that have that string in them
93
- role.rel_path.to_s.include? input
94
- }.tap {|matches|
95
- QB.debug "role matches" => matches
96
123
  }
124
+
125
+ # see if we prefix match any full names
126
+ name_prefix_matches = available.select {|role|
127
+ role.name.start_with? input
128
+ }
129
+ return name_prefix_matches unless name_prefix_matches.empty?
130
+
131
+ # see if we prefix match any name
132
+ namespaceless_prefix_matches = available.select {|role|
133
+ role.namespaceless.start_with? input
134
+ }
135
+ unless namespaceless_prefix_matches.empty?
136
+ return namespaceless_prefix_matches
137
+ end
138
+
139
+ # see if we word match any names
140
+ name_word_matches = available.select {|role|
141
+ QB::Util.words_start_with? role.name, input
142
+ }
143
+ return name_word_matches unless name_word_matches.empty?
144
+
145
+ []
97
146
  end
98
147
 
99
148
  # find exactly one matching role for the input string or raise.
@@ -0,0 +1,54 @@
1
+ module QB
2
+ module Util
3
+ # split a string into 'words' for word-based matching
4
+ def self.words string
5
+ string.split(/[\W_-]+/).reject {|w| w.empty?}
6
+ end # .words
7
+
8
+ # see if words from an input match words
9
+ def self.words_start_with? full_string, input
10
+ # QB.debug "does #{ input } match #{ full_string }?"
11
+
12
+ input_words = words input
13
+ full_string_words = words full_string
14
+
15
+ full_string_words.each_with_index {|word, start_index|
16
+ # compute the end index in full_string_words
17
+ end_index = start_index + input_words.length - 1
18
+
19
+ # short-circuit if can't match (more input words than full words left)
20
+ if end_index >= full_string_words.length
21
+ return false
22
+ end
23
+
24
+ # create the slice to test against
25
+ slice = full_string_words[start_index..end_index]
26
+
27
+ # see if every word in the slice starts with the corresponding word
28
+ # in the input
29
+ if slice.zip(input_words).all? {|full_word, input_word|
30
+ full_word.start_with? input_word
31
+ }
32
+ # got a match!
33
+ return true
34
+ end
35
+ }
36
+
37
+ # no match
38
+ false
39
+ end # .match_words?
40
+
41
+ # @return [Pathname] absolute resolved path.
42
+ def self.resolve *segments
43
+ joined = Pathname.new ''
44
+
45
+ ([Dir.pwd] + segments).reverse.each_with_index {|segment, index|
46
+ joined = Pathname.new(segment).join joined
47
+ return joined if joined.absolute?
48
+ }
49
+
50
+ # shouldn't ever happen
51
+ raise "resolution failed: #{ segments.inspect }"
52
+ end
53
+ end # Util
54
+ end # QB
@@ -0,0 +1,86 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'securerandom'
4
+ require 'fileutils'
5
+ require 'nrser'
6
+
7
+ using NRSER
8
+
9
+ module QB; end
10
+ module QB::Util; end
11
+
12
+ module QB::Util::STDIO
13
+ SOCKET_DIR = Pathname.new('/').join 'tmp', 'qb-stdio'
14
+
15
+ # STDIO as a service exposed on a UNIX socket so that modules can stream
16
+ # their output to it, which is in turn printed to the console `qb` is running
17
+ # in.
18
+ class Service
19
+ def initialize name, dest
20
+ @name = name
21
+ @dest = dest
22
+ @thread = nil
23
+ @server = nil
24
+ @socket = nil
25
+ @env_key = "QB_STDIO_#{ name.upcase }"
26
+
27
+ unless SOCKET_DIR.exist?
28
+ FileUtils.mkdir SOCKET_DIR
29
+ end
30
+
31
+ @path = SOCKET_DIR.join "#{ name }.#{ SecureRandom.uuid }.sock"
32
+
33
+ @debug_header = "#{ name }@#{ @path.to_s }"
34
+ end
35
+
36
+ def debug *args
37
+ QB.debug "#{ @debug_header }", *args
38
+ end
39
+
40
+ def open!
41
+ debug "opening..."
42
+
43
+ # make sure env var is not already set (basically just prevents you from
44
+ # accidentally opening two instances with the same name)
45
+ if ENV.key? @env_key
46
+ raise <<-END.squish
47
+ env already contains key #{ @env_key } with value #{ ENV[@env_key] }
48
+ END
49
+ end
50
+
51
+ @thread = Thread.new do
52
+ debug "thread started."
53
+
54
+ @server = UNIXServer.new @path.to_s
55
+ @socket = @server.accept
56
+
57
+ while (line = @socket.gets) do
58
+ @dest.puts line
59
+ end
60
+ end
61
+
62
+ # set the env key so children can find the socket path
63
+ ENV[@env_key] = @path.to_s
64
+ debug "set env var #{ @env_key }=#{ ENV[@env_key] }"
65
+
66
+ debug "service open."
67
+ end # open
68
+
69
+ def close!
70
+ # clean up.
71
+ #
72
+ # TODO not sure how correct this is...
73
+ #
74
+ debug "closing..."
75
+
76
+ @thread.kill
77
+ @socket.close
78
+ @socket = nil
79
+ @server.close
80
+ @server = nil
81
+ FileUtils.rm @path
82
+
83
+ debug "closed."
84
+ end
85
+ end # Service
86
+ end # QB::Util::STDIO
@@ -1,7 +1,7 @@
1
1
  module QB
2
2
  GEM_NAME = 'qb'
3
3
 
4
- VERSION = "0.1.36"
4
+ VERSION = "0.1.37"
5
5
 
6
6
  def self.gemspec
7
7
  Gem.loaded_specs[GEM_NAME]
data/qb.gemspec CHANGED
@@ -44,6 +44,7 @@ Gem::Specification.new do |spec|
44
44
  spec.add_dependency "cmds",'~> 0.0', ">= 0.0.9"
45
45
  spec.add_dependency "nrser-extras", '~> 0.0', ">= 0.0.3"
46
46
  spec.add_dependency "state_mate", '~> 0.0', ">= 0.0.9"
47
+ spec.add_dependency 'parseconfig', '~> 1.0', '>= 1.0.8'
47
48
 
48
49
 
49
50
  if QB::VERSION.end_with? '.dev'
@@ -0,0 +1,3 @@
1
+ ---
2
+ qb.qb_role:
3
+ readme: true
@@ -0,0 +1,5 @@
1
+ qb.git_submodule_update
2
+ =======================
3
+
4
+ init / update submodules, checking out branch if the commit points to the
5
+ head of exactly one.
@@ -0,0 +1,3 @@
1
+ ---
2
+ # defaults file for qb.git_submodule_update
3
+ git_submodule_update_dir: "{{ qb_dir }}"
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #!/usr/bin/env ruby
4
+ # WANT_JSON
5
+
6
+ # init bundler in dev env
7
+ if ENV['QB_DEV_ENV']
8
+ ENV.each {|k, v|
9
+ if k.start_with? 'QB_DEV_ENV_'
10
+ ENV[k.sub('QB_DEV_ENV_', '')] = v
11
+ end
12
+ }
13
+ require 'bundler/setup'
14
+ end
15
+
16
+ require 'qb'
17
+ require 'cmds'
18
+ require 'nrser'
19
+
20
+ class GitSubmoduleUpdate < QB::AnsibleModule
21
+ def main_dir
22
+ File.realpath @args['dir']
23
+ end
24
+
25
+ def resolve *path
26
+ QB::Util.resolve main_dir, *path
27
+ end
28
+
29
+ def submodules
30
+ out = Dir.chdir main_dir do
31
+ Cmds.out! "git submodule"
32
+ end
33
+
34
+ out.lines.map {|line|
35
+ match = line.match /([0-9a-f]{40})\s(\S+)\s/
36
+ commit = match[1]
37
+ rel_dir = match[2]
38
+ dir = resolve rel_dir
39
+
40
+ {
41
+ commit: commit,
42
+ dir: dir,
43
+ rel_dir: rel_dir,
44
+ detached: detached?(dir),
45
+ dirty: dirty?(dir),
46
+ }
47
+ }
48
+ end
49
+
50
+ def dirty? repo_dir
51
+ Dir.chdir repo_dir do
52
+ !Cmds.out!("git status --porcelain").empty?
53
+ end
54
+ end
55
+
56
+ def detached? repo_dir
57
+ Dir.chdir repo_dir do
58
+ out = Cmds.out! "git branch"
59
+
60
+ !!(out.lines[0].match /\*\ \(HEAD\ detached\ at [0-9a-f]{7}\)/)
61
+ end
62
+ end
63
+
64
+ def branch_heads repo_dir
65
+ Dir.chdir repo_dir do
66
+ Cmds.out!("git show-ref").lines.map {|line|
67
+ m = line.match(/([0-9a-f]{40})\s+(\S+)\s/)
68
+ commit = m[1]
69
+ ref = m[2]
70
+
71
+ {
72
+ commit: commit,
73
+ ref: ref,
74
+ }
75
+ }
76
+ end
77
+ end
78
+
79
+ def branch_heads_for_commit submodule
80
+ branch_heads(submodule[:dir]).select {|branch_head|
81
+ branch_head[:commit] == submodule[:commit]
82
+ }.reject {|branch_head| branch_head[:ref].end_with? 'HEAD'}
83
+ end
84
+
85
+ def attach! submodule
86
+ branch_heads = branch_heads_for_commit submodule
87
+ branch_head = nil
88
+
89
+ case branch_heads.length
90
+ when 0
91
+ # commit does not point to any branch heads - which means it's
92
+ # probably a commit in a branch that's behind the head
93
+ #
94
+ # we could figure out which branch it's in but we don't want to
95
+ # automatically update it because that might break shit.
96
+ #
97
+ # do nothing
98
+ return false
99
+ when 1
100
+ # commit is head of only one branch
101
+ branch_head = branch_heads[0]
102
+ else
103
+ # commit is head of multiple branches
104
+ local = branch_heads.select {|bh| bh[:ref].start_with? 'refs/heads'}
105
+
106
+ case local.length
107
+ when 0
108
+ # commit is head of multiple remote branches
109
+ # see if one is master
110
+ branch_head = branch_heads.find {|bh| bh[:ref].end_with? 'master'}
111
+
112
+ # if none do we're hosed - not sure which one it should be on
113
+ if branch_head.nil?
114
+ raise NRSER.squish <<-END
115
+ submodule #{ submodule[:rel_dir] } points to commit
116
+ #{ submodule[:commit] } that heads multiple non-master remote
117
+ branches: #{ branch_heads.map {|bh| bh[:ref]} }
118
+ END
119
+ end
120
+
121
+ when 1
122
+ # the commit is head of one local branch, use it
123
+ branch_head = local[0]
124
+
125
+ else
126
+ # the commit heads multiple local branches
127
+ # again, see if one is master
128
+ branch_head = local.find {|b| b[:ref].end_with? 'master'}
129
+
130
+ # if none do we're hosed - not sure which one it should be on
131
+ if branch_head.nil?
132
+ raise NRSER.squish <<-END
133
+ submodule #{ submodule[:rel_dir] } points to commit
134
+ #{ submodule[:commit] } that heads multiple non-master local
135
+ branches: #{ local.map {|bh| bh[:ref]} }
136
+ END
137
+ end
138
+ end # case
139
+ end
140
+
141
+ branch = branch_head[:ref].split('/')[-1]
142
+
143
+ Dir.chdir submodule[:dir] do
144
+ # checkout the branch
145
+ Cmds! "git checkout <%= branch %>", branch: branch
146
+
147
+ # do a pull if the head was on the remote
148
+ if branch_head[:ref].start_with? 'refs/remotes'
149
+ Cmds! "git pull origin <%= branch %>", branch: branch
150
+ end
151
+ end
152
+
153
+ @changed = true
154
+ end
155
+
156
+ def main
157
+ submodules.select {|sub|
158
+ sub[:detached]
159
+ }.each {|sub|
160
+ attach! sub
161
+ }
162
+
163
+ nil
164
+ end
165
+ end
166
+
167
+ GitSubmoduleUpdate.new.run
@@ -0,0 +1,8 @@
1
+ ---
2
+ # meta file for qb.git_submodule_update
3
+
4
+ allow_duplicates: yes
5
+
6
+ dependencies: []
7
+ # - role: role-name
8
+
@@ -0,0 +1,44 @@
1
+ ---
2
+ # meta/qb.yml file for qb.git_submodule_update
3
+ #
4
+ # qb settings for this role. see README.md for more info.
5
+ #
6
+
7
+ # description of the role to show in it's help output.
8
+ description: null
9
+
10
+ # prefix for role variables
11
+ var_prefix: null
12
+
13
+ # how to get a default for `dir` if it's not provided as the only
14
+ # positional argument. if a positional argument is provided it will
15
+ # override the method defined here.
16
+ #
17
+ # options:
18
+ #
19
+ # - null
20
+ # - require the value on the command line.
21
+ # - git_root
22
+ # - use the git root fof the directory that the `qb` command is invoked
23
+ # from. useful for 'project-centric' commands so they can be invoked
24
+ # from anywhere in the repo.
25
+ # - cwd
26
+ # - use the directory the `qb` command is invoked form.
27
+ # - {exe: PATH}
28
+ # - invoke an execuable, passing a JSON serialization of the options
29
+ # mapping their CLI names to values. path can be relative to role
30
+ # directory.
31
+ default_dir: git_root
32
+
33
+ # default user to become for play
34
+ default_user: null
35
+
36
+ # set to false to not save options in .qb-options.yml files
37
+ save_options: false
38
+
39
+ options: []
40
+ # - name: example
41
+ # description: an example of a variable.
42
+ # required: false
43
+ # type: boolean # boolean (default) | string
44
+ # short: e
@@ -0,0 +1,11 @@
1
+ ---
2
+ # tasks file for qb.git_submodule_update
3
+
4
+ - name: update submodules
5
+ command: git submodule update --init
6
+ args:
7
+ chdir: "{{ git_submodule_update_dir }}"
8
+
9
+ - name: checkout branch for any commits that are exactly one head
10
+ git_submodule_update:
11
+ dir: "{{ git_submodule_update_dir }}"
@@ -9,6 +9,7 @@ role_meta: true
9
9
  role_tasks: true
10
10
  role_templates: false
11
11
  role_vars: false
12
+ role_readme: false
12
13
 
13
14
  # galaxy
14
15
  role_galaxy: false
@@ -62,6 +62,11 @@ vars:
62
62
  description: include galaxy info in meta/main.yml
63
63
  short: g
64
64
 
65
+ - name: readme
66
+ type: boolean
67
+ description: include README.md
68
+ short: r
69
+
65
70
  - name: project
66
71
  type: boolean
67
72
  description: create a project repo for this role
@@ -115,3 +115,14 @@
115
115
  dest: "{{ dir }}/vars/main.yml"
116
116
  force: "{{ role_force }}"
117
117
  when: role_vars
118
+
119
+ # readme
120
+ # ======
121
+
122
+ - name: create README.md
123
+ template:
124
+ src: README.md.j2
125
+ dest: "{{ dir }}/README.md"
126
+ force: "{{ role_force }}"
127
+ when: role_readme
128
+
@@ -0,0 +1,4 @@
1
+ {{ role_role_name }}
2
+ {{ '=' * (role_role_name | length) }}
3
+
4
+ {{ role_role_name }} role.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.36
4
+ version: 0.1.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - nrser
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-01 00:00:00.000000000 Z
11
+ date: 2016-12-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -112,6 +112,26 @@ dependencies:
112
112
  - - ">="
113
113
  - !ruby/object:Gem::Version
114
114
  version: 0.0.9
115
+ - !ruby/object:Gem::Dependency
116
+ name: parseconfig
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.0'
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 1.0.8
125
+ type: :runtime
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.0'
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 1.0.8
115
135
  description:
116
136
  email:
117
137
  - neil@ztkae.com
@@ -133,23 +153,37 @@ files:
133
153
  - bin/console
134
154
  - bin/print-error
135
155
  - bin/qb
156
+ - bin/rake
136
157
  - bin/setup
137
158
  - bin/ungem
138
159
  - dev/ansible.cfg
139
160
  - dev/hosts
140
161
  - dev/requirements.yml
162
+ - dev/scratch/ansible_module/defaults/main.yml
163
+ - dev/scratch/ansible_module/library/test
164
+ - dev/scratch/ansible_module/meta/main.yml
165
+ - dev/scratch/ansible_module/meta/qb.yml
166
+ - dev/scratch/ansible_module/tasks/main.yml
141
167
  - dev/scratch/case.rb
142
168
  - dev/scratch/empty/defaults/main.yml
143
169
  - dev/scratch/empty/meta/main.yml
144
170
  - dev/scratch/empty/meta/qb.yml
145
171
  - dev/scratch/empty/tasks/main.yml
146
172
  - dev/scratch/stateSpec.js
173
+ - dev/scratch/stdio/defaults/main.yml
174
+ - dev/scratch/stdio/library/test
175
+ - dev/scratch/stdio/meta/main.yml
176
+ - dev/scratch/stdio/meta/qb.yml
177
+ - dev/scratch/stdio/tasks/main.yml
147
178
  - dev/setup.yml
148
179
  - exe/qb
149
180
  - lib/qb.rb
181
+ - lib/qb/ansible_module.rb
150
182
  - lib/qb/options.rb
151
183
  - lib/qb/options/option.rb
152
184
  - lib/qb/role.rb
185
+ - lib/qb/util.rb
186
+ - lib/qb/util/stdio.rb
153
187
  - lib/qb/version.rb
154
188
  - library/git_mkdir.py
155
189
  - library/qb_facts.py
@@ -283,6 +317,13 @@ files:
283
317
  - roles/qb.git_repo/meta/main.yml
284
318
  - roles/qb.git_repo/meta/qb.yml
285
319
  - roles/qb.git_repo/tasks/main.yml
320
+ - roles/qb.git_submodule_update/.qb-options.yml
321
+ - roles/qb.git_submodule_update/README.md
322
+ - roles/qb.git_submodule_update/defaults/main.yml
323
+ - roles/qb.git_submodule_update/library/git_submodule_update
324
+ - roles/qb.git_submodule_update/meta/main.yml
325
+ - roles/qb.git_submodule_update/meta/qb.yml
326
+ - roles/qb.git_submodule_update/tasks/main.yml
286
327
  - roles/qb.gitignore/defaults/main.yml
287
328
  - roles/qb.gitignore/files/gitignore/.github/PULL_REQUEST_TEMPLATE.md
288
329
  - roles/qb.gitignore/files/gitignore/Actionscript.gitignore
@@ -523,6 +564,7 @@ files:
523
564
  - roles/qb.role/meta/qb.yml
524
565
  - roles/qb.role/tasks/main.yml
525
566
  - roles/qb.role/templates/.gitkeep
567
+ - roles/qb.role/templates/README.md.j2
526
568
  - roles/qb.role/templates/defaults/main.yml.j2
527
569
  - roles/qb.role/templates/handlers/main.yml.j2
528
570
  - roles/qb.role/templates/meta/main.yml.j2