ding 0.4.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: 152cdf9f3aabb65b05c06221910c6260db5f1034
4
+ data.tar.gz: d687eb4faa485a64a53521a68613f32f4c29bee5
5
+ SHA512:
6
+ metadata.gz: 37a8445e13983b973f6017ef464af96e8363f854aa3bec7fdf066a819db1ff867f32a2d1a301b4645d117a4311eec3ddc7ef15b3943d1f23d0113dfb6b9b32cd
7
+ data.tar.gz: d8e834a7f6aed324918085c708e879d3fbd570c611d47bc96cfc75ea99f9a49a4e89e2514045b6b3ebaece22330982067368dfc0940dfdcf88459578acd53cf7
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ding.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Ding
2
+
3
+ ![Ding](./ding.png)
4
+
5
+ Simple command line tool for deploying a specific feature branch of a
6
+ repo to a testing branch for driving CI deployment for QA.
7
+
8
+ ## Installation
9
+
10
+ The usual method works:
11
+
12
+ gem install ding
13
+
14
+ ## Configuration
15
+
16
+ By default, `ding` will create a branch called `testing` from the
17
+ selected feature branch. It also assumes that the master branch is
18
+ called `master`. Branches named `master` and `develop` cannot be deleted
19
+ by calling `Ding::Git.delete_branch` in code.
20
+
21
+ These defaults can be over-ridden by providing ENV vars to the shell:
22
+
23
+ DING_MASTER_BRANCH - main branch to switch to for synchronising
24
+ DING_TESTING_BRANCH - branch to over-ride from feature branch
25
+ DING_SACROSANCT_BRANCHES - space separated list of protected branches
26
+
27
+ ## Using Ding
28
+
29
+ There are several commands available with global options for verbosity and forcing actions:
30
+
31
+ Commands:
32
+ ding help [COMMAND] # Describe available commands or one specific command
33
+ ding key-gen # Create a new private/public key pair and associated ssh config
34
+ ding key-show # Copy a public ssh key signature to the system clipboard (use -v to also display the signature)
35
+ ding test # Push a feature branch to the testing branch (this is the default action)
36
+
37
+ Options:
38
+ -f, [--force], [--no-force] # use the force on commands that allow it e.g. git push
39
+ -v, [--verbose], [--no-verbose] # show verbose output such as full callstack on errors
40
+
41
+ ### ding test
42
+
43
+ This is the default action so running `ding` is the equivalent of `ding test`.
44
+
45
+ There is an option to specify the feature branch pattern to display for
46
+ selection of the code to be pushed to `testing`.
47
+
48
+ $ ding help test
49
+
50
+ Usage:
51
+ ding test
52
+
53
+ Options:
54
+ -p, [--pattern=PATTERN] # specify a pattern for listing branches
55
+ # Default: origin/XAP*
56
+
57
+ Push a feature branch to the testing branch
58
+
59
+ ### ding key-gen
60
+
61
+ This will generate a new ssh key pair and configure them into the ssh config
62
+ for the relevant host. This allows `ding test` to push code to bitbucket.org,
63
+ for example, so that you aren't prompted for a userid and password each
64
+ time.
65
+
66
+ On completion, the public key is copied to the system clipboard so that
67
+ it can be pasted into the users account on bitbucket.org.
68
+
69
+ Usage:
70
+ ding key-gen
71
+
72
+ Options:
73
+ -h, [--host=HOST] # specify repository host for ssh config
74
+ # Default: bitbucket.org
75
+ -n, [--name=NAME] # name for key, defaults to host name
76
+ -p, [--passphrase=PASSPHRASE] # optional passphrase for key
77
+ -t, [--type=TYPE] # type of key to create per -t option on ssh-keygen
78
+ # Default: rsa
79
+
80
+ Create a new private/public key pair and associated ssh config
81
+
82
+ ### ding key-show
83
+
84
+ If the public key is needed again for pasting into the bitbucket.org config it can be
85
+ captured on the clipboard by running this command and selecting the appropriate key from
86
+ the list presented.
87
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/ding ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "ding"
6
+
7
+ Ding::Cli.start
data/ding.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ding/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ding'
8
+ spec.version = Ding::VERSION
9
+ spec.authors = ['Warren Bain']
10
+ spec.email = ['warren@thoughtcroft.com']
11
+
12
+ spec.summary = %q{Push specific feature branch code to a testing branch}
13
+ spec.description = %q{Push specific feature branch code to a testing branch}
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.10'
21
+ spec.add_development_dependency 'rake', '~> 10.0'
22
+ spec.add_runtime_dependency 'thor', '~> 0.19'
23
+ spec.add_runtime_dependency 'git-up'
24
+ end
data/ding.png ADDED
Binary file
data/lib/ding.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'ding/version'
2
+ require 'ding/cli'
3
+ require 'ding/models/git'
4
+ require 'ding/models/ssh'
5
+
6
+ module Ding
7
+ MASTER_BRANCH = ENV['DING_MASTER_BRANCH'] || 'master'
8
+ TESTING_BRANCH = ENV['DING_TESTING_BRANCH'] || 'testing'
9
+ SACROSANCT_BRANCHES = (ENV['DING_SACROSANCT_BRANCHES'] || 'master develop').split
10
+
11
+ # because we lurve the command line... ding!
12
+ end
data/lib/ding/cli.rb ADDED
@@ -0,0 +1,144 @@
1
+ require 'shellwords'
2
+ require 'thor'
3
+
4
+ module Ding
5
+ class Cli < Thor
6
+ class_option :force, type: 'boolean', aliases: '-f', default: false, desc: 'use the force on commands that allow it e.g. git push'
7
+ class_option :verbose, type: 'boolean', aliases: '-v', default: false, desc: 'show verbose output such as full callstack on errors'
8
+
9
+ default_task :test
10
+
11
+ desc "test", "Push a feature branch to the testing branch (this is the default action)"
12
+ option :pattern, type: 'string', aliases: '-p', default: 'origin/XAP*', desc: 'specify a pattern for listing branches'
13
+ def test
14
+ master_branch, testing_branch = Ding::MASTER_BRANCH.dup, Ding::TESTING_BRANCH.dup
15
+ say "\nDing ding ding: let's push a feature branch to #{testing_branch}...\n\n", :green
16
+
17
+ repo = Ding::Git.new(options).tap do |r|
18
+ say "> Synchronising with the remote...", :green
19
+ r.checkout master_branch
20
+ r.update
21
+ end
22
+
23
+ branches = repo.branches(options[:pattern])
24
+ if branches.empty?
25
+ say "\n --> No feature branches available to test, I'm out of here!\n\n", :red
26
+ exit 1
27
+ end
28
+
29
+ feature_branch = ask_which_item(branches, 'Which feature branch should I use?')
30
+
31
+ repo.tap do |r|
32
+ say "\n> Deleting #{testing_branch}...", :green
33
+ r.delete_branch(testing_branch)
34
+ say "> Checking out #{feature_branch}...", :green
35
+ r.checkout(feature_branch)
36
+ say "> Creating #{testing_branch}...", :green
37
+ r.create_branch(testing_branch)
38
+ say "> Pushing #{testing_branch} to the remote...", :green
39
+ r.push(testing_branch)
40
+ end
41
+
42
+ rescue => e
43
+ show_error e
44
+ else
45
+ say "\n --> I'm finished: ding ding ding!\n\n", :green
46
+ end
47
+
48
+ desc "key-gen", "Create a new private/public key pair and associated ssh config"
49
+ option :host, type: 'string', aliases: '-h', default: 'bitbucket.org', desc: 'specify repository host for ssh config'
50
+ option :name, type: 'string', aliases: '-n', default: nil, desc: 'name for key, defaults to host name'
51
+ option :passphrase, type: 'string', aliases: '-p', default: '', desc: 'optional passphrase for key'
52
+ option :type, type: 'string', aliases: '-t', default: 'rsa', desc: 'type of key to create per -t option on ssh-keygen'
53
+ def key_gen
54
+ key_name = options[:name] || "#{options[:host]}_#{options[:type]}"
55
+ say "\nDing ding ding: let's create and configure a new ssh key #{key_name}...\n\n", :green
56
+
57
+ Ding::Ssh.new(options).tap do |s|
58
+ if s.ssh_key_exists?(key_name)
59
+ if yes?("Do you want me to replace the existing key?", :yellow)
60
+ say "> Removing existing key #{key_name}...", :cyan
61
+ s.delete_ssh_key key_name
62
+ say "> Creating the replacement ssh key pair...", :cyan
63
+ s.create_ssh_key key_name, ENV['USER']
64
+ else
65
+ say "> Using existing key #{key_name}...", :cyan
66
+ end
67
+ else
68
+ say "> Creating the new ssh key pair...", :green
69
+ s.create_ssh_key key_name, ENV['USER']
70
+ end
71
+ say "> Adding the private key to the ssh config...", :green
72
+ s.update_config options[:host], key_name
73
+ say "> Copying the public key to the clipboard...", :green
74
+ copy_file_to_clipboard s.ssh_public_key_file(key_name)
75
+ end
76
+
77
+ rescue => e
78
+ show_error e
79
+ else
80
+ say "\n --> I'm finished: ding ding ding!\n\n", :green
81
+ end
82
+
83
+ desc "key-show", "Copy a public ssh key signature to the system clipboard (use -v to also display the signature)"
84
+ def key_show
85
+ say "\nDing ding ding: let's copy a public key to the clipboard...\n\n", :green
86
+
87
+ Ding::Ssh.new(options).tap do |s|
88
+ key_name = ask_which_item(s.list_ssh_keys, 'Which key do you want to copy?')
89
+ say "\n> Copying the public key to the clipboard...", :green
90
+ copy_file_to_clipboard s.ssh_public_key_file(key_name)
91
+ end
92
+
93
+ rescue => e
94
+ show_error e
95
+ else
96
+ say "\n --> You can now Command-V to paste that key: ding ding ding!\n\n", :green
97
+ end
98
+
99
+ private
100
+
101
+ def show_error(e)
102
+ say "\n --> Error: #{e.message}\n\n", :red
103
+ raise if options[:verbose]
104
+ exit 1
105
+ end
106
+
107
+ def ask_which_item(items, prompt)
108
+ return items.first if items.size == 1
109
+ str_format = "\n %#{items.count.to_s.size}s: %s"
110
+ question = set_color prompt, :yellow
111
+ answers = {}
112
+
113
+ items.each_with_index do |item, index|
114
+ i = (index + 1).to_s
115
+ answers[i] = item
116
+ question << format(str_format, i, item)
117
+ end
118
+
119
+ say question
120
+ reply = ask("> ").to_s
121
+ if answers[reply]
122
+ answers[reply]
123
+ else
124
+ say "\n --> That's not a valid selection, I'm out of here!\n\n", :red
125
+ exit 1
126
+ end
127
+ end
128
+
129
+ def copy_file_to_clipboard(file)
130
+ cmd = "cat #{file} | "
131
+ if options[:verbose]
132
+ cmd << 'tee >(pbcopy)'
133
+ else
134
+ cmd << ' pbcopy'
135
+ end
136
+ bash cmd
137
+ end
138
+
139
+ def bash(cmd)
140
+ escaped_cmd = Shellwords.escape cmd
141
+ system "bash -c #{escaped_cmd}"
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,99 @@
1
+ module Ding
2
+ class Git
3
+
4
+ def initialize(options={})
5
+ raise "#{repo} is NOT a git repository" unless git_repo?
6
+ @options = options
7
+ end
8
+
9
+ def branches(pattern)
10
+ %x(git branch --remote --list #{remote_version(pattern)}).split.map {|b| b.split('/').last}
11
+ end
12
+
13
+ def branch_exists?(branch)
14
+ ! %x(git branch --list #{branch}).empty?
15
+ end
16
+
17
+ def checkout(branch)
18
+ raise "Unable to checkout #{branch}" unless run_cmd "git checkout #{branch}"
19
+ end
20
+
21
+ def create_branch(branch)
22
+ raise "Unable to create #{branch}" unless run_cmd "git branch --track #{branch}"
23
+ end
24
+
25
+ def delete_branch(branch)
26
+ local_branch = local_version(branch)
27
+ remote_branch = remote_version(branch)
28
+ raise "You are not allowed to delete #{local_branch}" if Ding::SACROSANCT_BRANCHES.include?(local_branch)
29
+ if branch_exists?(local_branch)
30
+ branch_cmd = "git branch #{options[:force] ? '-D' : '-d'} #{local_branch}"
31
+ raise "Unable to delete #{local_branch}" unless run_cmd branch_cmd
32
+ end
33
+ if branch_exists?(remote_branch)
34
+ branch_cmd = "git push #{remote_name} :#{local_branch} #{options[:force] ? '-f' : ''}"
35
+ raise "Unable to delete #{remote_branch}" unless run_cmd branch_cmd
36
+ end
37
+ end
38
+
39
+ def push(branch)
40
+ checkout branch
41
+ push_cmd = "git push #{remote_name} #{branch}"
42
+ push_cmd << " --force" if options[:force]
43
+ raise "Unable to push #{branch} branch!" unless run_cmd push_cmd
44
+ end
45
+
46
+ def update
47
+ raise "Error synchronising with the remote" unless run_cmd "git up"
48
+ end
49
+
50
+ private
51
+
52
+ def git_repo?
53
+ run_cmd "git status"
54
+ end
55
+
56
+ def repo
57
+ @repo || Dir.pwd
58
+ end
59
+
60
+ def remote_version(branch)
61
+ if is_remote?(branch)
62
+ branch
63
+ else
64
+ "#{remote_prefix}#{branch}"
65
+ end
66
+ end
67
+
68
+ def local_version(branch)
69
+ if is_remote?(branch)
70
+ branch.gsub(remote_name, '')
71
+ else
72
+ branch
73
+ end
74
+ end
75
+
76
+ def is_remote?(branch)
77
+ branch.start_with?(remote_prefix)
78
+ end
79
+
80
+ def remote_name
81
+ @remote_name || %x(git remote).chomp
82
+ end
83
+
84
+ def remote_prefix
85
+ "#{remote_name}/"
86
+ end
87
+
88
+ # NOTE: only for commands where we are interested in the effect
89
+ # as unless verbose is turned on, stdout and stderr are suppressed
90
+ def run_cmd(cmd)
91
+ cmd << ' &>/dev/null ' unless options[:verbose]
92
+ system cmd
93
+ end
94
+
95
+ def options
96
+ @options || {}
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,75 @@
1
+ require 'fileutils'
2
+
3
+ module Ding
4
+ class Ssh
5
+
6
+ def initialize(options={})
7
+ @options = options
8
+ end
9
+
10
+ def list_ssh_keys
11
+ Dir.glob(File.join(ssh_config_path, '*.pub')).map {|f| File.basename f, '.pub'}
12
+ end
13
+
14
+ def create_ssh_key(name, comment)
15
+ raise "ssh key #{name} already exists!" if ssh_key_exists? name
16
+ run_cmd "ssh-keygen -t #{options[:type]} -C #{comment} -P '#{options[:passphrase]}' -f #{File.join(ssh_config_path, name)}"
17
+ end
18
+
19
+ def delete_ssh_key(name)
20
+ File.delete ssh_public_key_file(name), ssh_private_key_file(name) if ssh_key_exists? name
21
+ end
22
+
23
+ def update_config(host, name)
24
+ if File.exists?(ssh_config_file)
25
+ config = File.open(ssh_config_file).read
26
+ raise "Host #{host} already configured in ssh config" if config.include?(host)
27
+ raise "Key #{name} already configured in ssh config" if config.include?(name)
28
+ else
29
+ FileUtils.mkdir_p ssh_config_path
30
+ end
31
+
32
+ File.open(ssh_config_file, 'a') do |f|
33
+ f.puts "Host #{host}"
34
+ f.puts " IdentityFile #{ssh_private_key_file name}"
35
+ end
36
+ end
37
+
38
+ def ssh_key_exists?(name)
39
+ File.exists? ssh_private_key_file(name)
40
+ end
41
+
42
+ def ssh_private_key_file(name)
43
+ File.join ssh_config_path, name
44
+ end
45
+
46
+ def ssh_public_key_file(name)
47
+ "#{ssh_private_key_file name}.pub"
48
+ end
49
+
50
+ private
51
+
52
+ def ssh_config_exists?
53
+ File.exists? ssh_config_file
54
+ end
55
+
56
+ def ssh_config_path
57
+ @ssh_config_path || options[:ssh_config_path] || File.join(ENV['HOME'], '.ssh')
58
+ end
59
+
60
+ def ssh_config_file
61
+ @ssh_config_file || options[:ssh_config_file] || File.join(ssh_config_path, 'config')
62
+ end
63
+
64
+ # NOTE: only for commands where we are interested in the effect
65
+ # as unless verbose is turned on, stdout and stderr are suppressed
66
+ def run_cmd(cmd)
67
+ cmd << ' &>/dev/null ' unless options[:verbose]
68
+ system cmd
69
+ end
70
+
71
+ def options
72
+ @options || {}
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module Ding
2
+ VERSION = "0.4.0"
3
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ding
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Warren Bain
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.19'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.19'
55
+ - !ruby/object:Gem::Dependency
56
+ name: git-up
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Push specific feature branch code to a testing branch
70
+ email:
71
+ - warren@thoughtcroft.com
72
+ executables:
73
+ - ding
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - bin/ding
84
+ - ding.gemspec
85
+ - ding.png
86
+ - lib/ding.rb
87
+ - lib/ding/cli.rb
88
+ - lib/ding/models/git.rb
89
+ - lib/ding/models/ssh.rb
90
+ - lib/ding/version.rb
91
+ homepage:
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.4.5
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Push specific feature branch code to a testing branch
115
+ test_files: []