qb 0.1.36 → 0.1.37

Sign up to get free protection for your applications and to get access to all the features.
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