git_bpf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ test
2
+ .DS_Store
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in git_bpf.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2013, Affinity Bridge
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ The views and conclusions contained in the software and documentation are those
25
+ of the authors and should not be interpreted as representing official policies,
26
+ either expressed or implied, of the FreeBSD Project.
@@ -0,0 +1,67 @@
1
+ Affinity Bridge's Branch-per-Feature Scripts
2
+ ============================================
3
+
4
+ Configure a repository and add some useful [Branch-per-Feature] workflow commands.
5
+
6
+ Performs the following actions in the target repository:
7
+
8
+ - enables ```git-rerere```
9
+ - configures ```git-rerere``` to automatically stage successful resolutions
10
+ - a .git/rr-cache directory will be set up to synchronize with a 'rr-cache' branch in the repository's remote.
11
+ - installs a ```post-merge``` git hook for automatic rr-cache syncing
12
+ - installs the bundled branch-per-feature helper commands
13
+
14
+ ## Commands
15
+
16
+ ### git-recreate-branch
17
+
18
+ Usage: ```git recreate-branch <source-branch> [OPTIONS]...```
19
+
20
+ Recreates <source-branch> in-place or as a new branch by re-merging all of the merge commits which it is comprised of.
21
+
22
+ OPTIONS
23
+ -a, --base NAME A reference to the commit from which the source branch is based, defaults to master.
24
+ -b, --branch NAME Instead of deleting the source branch and replacng it with a new branch of the same name, leave the source branch and create a new branch called NAME.
25
+ -x, --exclude NAME Specify a list of branches to be excluded.
26
+ -l, --list Process source branch for merge commits and list them. Will not make any changes to any branches.
27
+
28
+
29
+ ### git-share-rerere
30
+
31
+ A collection of commands to help share your rr-cache.
32
+
33
+ OPTIONS
34
+ -c, --cache_dir DIR The location of your rr-cache dir, defaults to .git/rr-cache.
35
+ -g, --git-dir DIR The location of your rr-cache .git dir, defaults to .git/rr-cache/.git.
36
+ -b, --branch NAME The name of the branch your rr-cache is stored in, defaults to rr-cache.
37
+ -r, --remote NAME The name of the remote to use when getting the latest rr-cache, defaults to origin.
38
+
39
+ **Sub-commands - Usage:**
40
+
41
+ ```git share-rerere push```
42
+
43
+ Push any new resolutions to the designated <branch> on the remote.
44
+
45
+ ```git share-rerere pull```
46
+
47
+ Pull any new resolutions to the designated <branch> on the remote.
48
+
49
+ ## Install
50
+
51
+ _Requires git >= 1.7.10.x_
52
+
53
+ ### Install git-bpf-init script
54
+
55
+ git_bpf is packaged as a Ruby Gem and hosted on [RubyGems]
56
+ gem install git_bpf
57
+
58
+ ### Usage
59
+
60
+ git-bpf-init <target-repository>
61
+
62
+ - If <target-repository> is not provided, <target-repository> defaults to your current directory (will fail if current directory is not a git repository).
63
+ - The script requires the <target-repository> to have a remote named 'origin'.
64
+
65
+
66
+ [Branch-per-Feature]: https://github.com/affinitybridge/git-bpf/wiki/Branch-per-feature-process
67
+ [RubyGems]: http://rubygems.org/
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'git_bpf'
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'git_bpf'
4
+ GitFlow.should_run = false
5
+
6
+ at_exit { GitFlow.run(*(['init'] + ARGV)) }
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'git_bpf/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "git_bpf"
8
+ spec.version = GitBpf::VERSION
9
+ spec.authors = ["tnightingale"]
10
+ spec.email = ["tom@affinitybridge.com"]
11
+ spec.description = %q{A collection of commands to help with implementing the branch-per-feature git development workflow.}
12
+ spec.summary = %q{Git branch-per-feature helper commands.}
13
+ spec.homepage = "https://github.com/affinitybridge/git-bpf"
14
+ spec.license = "BSD"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ base_path = File.dirname(__FILE__)
3
+ $LOAD_PATH.unshift(base_path) unless $LOAD_PATH.include?(base_path)
4
+
@@ -0,0 +1,8 @@
1
+ require "git_bpf/version"
2
+
3
+ require "git_bpf/commands/recreate-branch"
4
+ require "git_bpf/commands/share-rerere-cache"
5
+ require "git_bpf/commands/init"
6
+
7
+ module GitBpf
8
+ end
@@ -0,0 +1,146 @@
1
+ require 'git_bpf/lib/gitflow'
2
+ require 'git_bpf/lib/git-helpers'
3
+ require 'git_bpf/lib/repository'
4
+
5
+ #
6
+ # init:
7
+ #
8
+ class Init < GitFlow/'init'
9
+
10
+ include GitHelpersMixin
11
+
12
+ @documentation = ""
13
+
14
+ def options(opts)
15
+ opts.script_dir_name = 'git-bpf'
16
+ opts.remote_name = 'origin'
17
+ opts.rerere_branch = 'rr-cache'
18
+
19
+ [
20
+ ['-d', '--directory-name NAME',
21
+ "",
22
+ lambda { |n| opts.script_dir_name = n }],
23
+ ['-r', '--remote-name NAME',
24
+ "",
25
+ lambda { |n| opts.remote_name = n }],
26
+ ['-b', '--rerere-branch NAME',
27
+ "",
28
+ lambda { |n| opts.rerere_branch = n }],
29
+ ]
30
+ end
31
+
32
+ def execute(opts, argv)
33
+ if argv.length > 1
34
+ run 'init', '--help'
35
+ terminate
36
+ end
37
+
38
+ # TODO: There's likely a better way to do this.
39
+ source_path = File.join File.dirname(__FILE__), '..'
40
+ target = Repository.new(argv.length == 1 ? argv.pop : Dir.getwd)
41
+
42
+
43
+ #
44
+ # 1. Link source scripts directory.
45
+ #
46
+ ohai "1. Linking scripts directory to '#{source_path}'."
47
+
48
+ scripts = File.join(target.path, '.git', opts.script_dir_name)
49
+
50
+ if not File.exists? scripts
51
+ File.symlink source_path, scripts
52
+ elsif File.symlink? scripts
53
+ opoo "Symbolic link already exists."
54
+ else
55
+ terminate "Cannot create symbolic link (#{scripts})."
56
+ end
57
+
58
+
59
+ #
60
+ # 2. Create aliases for commands.
61
+ #
62
+ commands = [
63
+ 'recreate-branch',
64
+ 'share-rerere',
65
+ ]
66
+
67
+ ohai "2. Creating aliases for commands:", commands.shell_list
68
+
69
+ commands.each do |name|
70
+ command = "!_git-bpf #{name}"
71
+ target.cmd("config", "--local", "alias.#{name}", command)
72
+ end
73
+
74
+
75
+ #
76
+ # 3. Set up rerere sharing.
77
+ #
78
+ ohai "3. Setting up rerere sharing."
79
+
80
+ target.config(true, "rerere.enabled", "true")
81
+ target.config(true, "rerere.autoupdate", "true")
82
+
83
+ rerere_path = File.join(target.git_dir, 'rr-cache')
84
+ target_remote_url = target.remoteUrl(opts.remote_name)
85
+
86
+ if not File.directory? rerere_path
87
+ rerere = Repository::clone target_remote_url, rerere_path
88
+ elsif not File.directory? File.join(rerere_path, '.git')
89
+ opoo "Rerere cache directory already exists; Initializing repository in existing rr-cache directory."
90
+ rerere = Repository.init rerere_path
91
+ rerere.cmd("remote", "add", opts.remote_name, target_remote_url)
92
+ else
93
+ opoo "Rerere cache directory already exists and is a repository."
94
+ rerere = Repository.new rerere_path
95
+ end
96
+
97
+ rerere.fetch opts.remote_name
98
+
99
+ if rerere.branch?('rr-cache', opts.remote_name)
100
+ # Remote has branch 'rr-cache', make sure we are currently on it.
101
+ if not rerere.head.include? "rr-cache"
102
+ rerere.cmd("checkout", "rr-cache")
103
+ end
104
+ else
105
+ # Create orphan branch 'rr-cache' and push to remote.
106
+ rerere.cmd("checkout", "--orphan", "rr-cache")
107
+ rerere.cmd("rm", "-rf", "--ignore-unmatch", "#{rerere_path}/")
108
+ rerere.cmd("commit", "-a", "--allow-empty", "-m", "Automatically creating branch to track conflict resolutions.")
109
+ rerere.cmd("push", opts.remote_name, "rr-cache")
110
+ end
111
+
112
+
113
+ #
114
+ # 4. Symlink git-hooks.
115
+ #
116
+ hooks_dir = File.join(target.git_dir, "hooks")
117
+ hooks = [
118
+ 'post-merge',
119
+ 'post-checkout'
120
+ ]
121
+
122
+ ohai "4. Creating symbolic links to git-hooks:", hooks.shell_list
123
+
124
+ hooks.each do |name|
125
+ target_hook_path = File.join(hooks_dir, name)
126
+ source_hook_path = File.join(scripts, "hooks", "#{name}.rb")
127
+ files = Dir.glob("#{target_hook_path}*")
128
+ write = files.empty?
129
+
130
+ if not write and promptYN "Existing hook '#{name}' detected, overwrite?"
131
+ write = File.delete(files.shell_s) > 0
132
+ end
133
+
134
+ if write
135
+ File.symlink source_hook_path, target_hook_path
136
+ else
137
+ opoo "Couldn't link '#{name}' hook as it already exists."
138
+ end
139
+ end
140
+
141
+ #
142
+ # Success!
143
+ #
144
+ ohai "Success!"
145
+ end
146
+ end
@@ -0,0 +1,179 @@
1
+ require 'git_bpf/lib/gitflow'
2
+ require 'git_bpf/lib/git-helpers'
3
+
4
+ #
5
+ # recreate_branch: Recreate a branch based on the merge commits it's comprised of.
6
+ #
7
+ class RecreateBranch < GitFlow/'recreate-branch'
8
+
9
+ include GitHelpersMixin
10
+
11
+ @@prefix = "BRANCH-PER-FEATURE-PREFIX"
12
+
13
+ @documentation = "Recreates the source branch in place or as a new branch by re-merging all of the merge commits."
14
+
15
+
16
+ def options(opts)
17
+ opts.base = 'master'
18
+ opts.exclude = []
19
+
20
+ [
21
+ ['-a', '--base NAME',
22
+ "A reference to the commit from which the source branch is based, defaults to #{opts.base}.",
23
+ lambda { |n| opts.base = n }],
24
+ ['-b', '--branch NAME',
25
+ "Instead of deleting the source branch and replacng it with a new branch of the same name, leave the source branch and create a new branch called NAME.",
26
+ lambda { |n| opts.branch = n }],
27
+ ['-x', '--exclude NAME',
28
+ "Specify a list of branches to be excluded.",
29
+ lambda { |n| opts.exclude.push(n) }],
30
+ ['-l', '--list',
31
+ "Process source branch for merge commits and list them. Will not make any changes to any branches.",
32
+ lambda { |n| opts.list = true }],
33
+ ]
34
+ end
35
+
36
+ def execute(opts, argv)
37
+ if argv.length != 1
38
+ run('recreate-branch', '--help')
39
+ terminate
40
+ end
41
+
42
+ source = argv.pop
43
+
44
+ # If no new branch name provided, replace the source branch.
45
+ opts.branch = source if opts.branch == nil
46
+
47
+ # Perform some validation.
48
+ if not branchExists? source
49
+ terminate "Cannot recreate branch #{source} as it doesn't exist."
50
+ end
51
+
52
+ if opts.branch != source and branchExists? opts.branch
53
+ terminate "Cannot create branch #{opts.branch} as it already exists."
54
+ end
55
+
56
+ if not refExists? opts.base
57
+ terminate "Cannot find reference '#{opts.base}' to use as a base for new branch: #{opts.branch}."
58
+ end
59
+
60
+ #
61
+ # 1. Compile a list of merged branches from source branch.
62
+ #
63
+ ohai "1. Processing branch '#{source}' for merge-commits..."
64
+
65
+ branches = getMergedBranches(opts.base, source)
66
+
67
+ if branches.empty?
68
+ terminate "No feature branches detected, '#{source}' matches '#{opts.base}'."
69
+ end
70
+
71
+ if opts.list
72
+ terminate "Branches to be merged:\n#{branches.shell_list}"
73
+ end
74
+
75
+ # Remove from the list any branches that have been explicity excluded using
76
+ # the -x option
77
+ branches.reject! do |item|
78
+ stripped = item.gsub /^remotes\/\w+\/([\w\-\/]+)$/, '\1'
79
+ opts.exclude.include? stripped
80
+ end
81
+
82
+ # Prompt to continue.
83
+ opoo "The following branches will be merged when the new #{opts.branch} branch is created:\n#{branches.shell_list}"
84
+ puts
85
+ puts "If you see something unexpected check:"
86
+ puts "a) that your '#{source}' branch is up to date"
87
+ puts "b) if '#{opts.base}' is a branch, make sure it is also up to date."
88
+ opoo "If there are any non-merge commits in '#{source}', they will not be included in '#{opts.branch}'. You have been warned."
89
+ if not promptYN "Proceed with #{source} branch recreation?"
90
+ terminate "Aborting."
91
+ end
92
+
93
+ #
94
+ # 2. Backup existing local source branch.
95
+ #
96
+ tmp_source = "#{@@prefix}-#{source}"
97
+ ohai "2. Creating backup of '#{source}', '#{tmp_source}'..."
98
+
99
+ if branchExists? tmp_source
100
+ terminate "Cannot create branch #{tmp_source} as one already exists. To continue, #{tmp_source} must be removed."
101
+ end
102
+
103
+ git('branch', '-m', source, tmp_source)
104
+
105
+ #
106
+ # 3. Create new branch based on 'base'.
107
+ #
108
+ ohai "3. Creating new '#{opts.branch}' branch based on '#{opts.base}'..."
109
+
110
+ git('checkout', '-b', opts.branch, opts.base, '--quiet')
111
+
112
+ #
113
+ # 4. Begin merging in feature branches.
114
+ #
115
+ ohai "4. Merging in feature branches..."
116
+
117
+ branches.each do |branch|
118
+ begin
119
+ puts " - '#{branch}'"
120
+ # Attempt to merge in the branch. If there is no conflict at all, we
121
+ # just move on to the next one.
122
+ git('merge', '--quiet', '--no-ff', '--no-edit', branch)
123
+ rescue
124
+ # There was a conflict. If there's no available rerere for it then it is
125
+ # unresolved and we need to abort as there's nothing that can be done
126
+ # automatically.
127
+ conflicts = git('rerere', 'status').chomp.split("\n")
128
+
129
+ if conflicts.length != 0
130
+ puts "\n"
131
+ puts "There is a merge conflict with branch #{branch} that has no rerere."
132
+ puts "Record a resoloution by resolving the conflict."
133
+ puts "Then run the following command to return your repository to its original state."
134
+ puts "\n"
135
+ puts "git checkout #{tmp_source} && git branch -D #{opts.branch} && git branch -m #{opts.branch}"
136
+ puts "\n"
137
+ puts "If you do not want to resolve the conflict, it is safe to just run the above command to restore your repository to the state it was in before executing this command."
138
+ terminate
139
+ else
140
+ # Otherwise, we have a rerere and the changes have been staged, so we
141
+ # just need to commit.
142
+ git('commit', '-a', '--no-edit')
143
+ end
144
+ end
145
+ end
146
+
147
+ #
148
+ # 5. Clean up.
149
+ #
150
+ ohai "5. Cleaning up temporary branches ('#{tmp_source}')."
151
+
152
+ if source != opts.branch
153
+ git('branch', '-m', tmp_source, source)
154
+ else
155
+ git('branch', '-D', tmp_source)
156
+ end
157
+ end
158
+
159
+ def getMergedBranches(base, source)
160
+ branches = []
161
+ merges = git('rev-list', '--parents', '--merges', '--reverse', "#{base}...#{source}").strip
162
+
163
+ merges.split("\n").each do |commits|
164
+ parents = commits.split("\s")
165
+ commit = parents.shift
166
+
167
+ parents.each do |parent|
168
+ name = git('name-rev', parent, '--name-only').strip
169
+ alt_base = git('name-rev', base, '--name-only').strip
170
+ remote_heads = /remote\/\w+\/HEAD/
171
+ unless name.include? source or name.include? alt_base or name.match remote_heads
172
+ branches.push name
173
+ end
174
+ end
175
+ end
176
+
177
+ return branches
178
+ end
179
+ end
@@ -0,0 +1,82 @@
1
+ require 'git_bpf/lib/gitflow'
2
+ require 'git_bpf/lib/git-helpers'
3
+ require 'git_bpf/lib/repository'
4
+
5
+ module ShareReReReMixin
6
+ def options(opts)
7
+ opts.work_tree = ".git/rr-cache"
8
+ opts.branch = "rr-cache"
9
+ opts.remote = "origin"
10
+
11
+ [
12
+ ['-c', '--cache_dir DIR',
13
+ "The location of your rr-cache dir, defaults to #{opts.work_tree}.",
14
+ lambda { |n| opts.rr_cache_dir = n }],
15
+ ['-b', '--branch NAME',
16
+ "The name of the branch your rr-cache is stored in, defaults to #{opts.branch}.",
17
+ lambda { |n| opts.branch = n }],
18
+ ['-r', '--remote NAME',
19
+ "The name of the remote to use when getting the latest rr-cache, defaults to #{opts.remote}.",
20
+ lambda { |r| opts.remote = r }],
21
+ ]
22
+ end
23
+ end
24
+
25
+ #
26
+ # share-rerere: Recreate a branch based on the merge commits it's comprised of.
27
+ #
28
+ class ShareReReRe < GitFlow/'share-rerere'
29
+
30
+ @documentation = <<-HELP.undent
31
+ A collection of commands to help share your rr-cache.
32
+
33
+ Available commands:
34
+ - push
35
+ - pull
36
+ HELP
37
+
38
+ def execute(opts, argv)
39
+ run('share-rerere', '--help')
40
+ end
41
+
42
+ class PullReReRe < ShareReReRe/'pull'
43
+
44
+ @help = "Pull the latest conflict resolutions."
45
+
46
+ include GitHelpersMixin
47
+ include ShareReReReMixin
48
+
49
+ def execute(opts, argv)
50
+ rerere = Repository.new opts.work_tree
51
+ rerere.cmd("pull", '--quiet', opts.remote, opts.branch)
52
+ end
53
+ end
54
+
55
+ class PushReReRe < ShareReReRe/'push'
56
+
57
+ @help = "Push your latest conflict resolutions."
58
+
59
+ include GitHelpersMixin
60
+ include ShareReReReMixin
61
+
62
+ def execute(opts, argv)
63
+ rerere = Repository.new opts.work_tree
64
+ lines = rerere.cmd("status", "--porcelain").split("\n").map { |a| a.chomp }
65
+ if lines.empty?
66
+ terminate "No resolutions to share."
67
+ end
68
+
69
+ lines.each do |line|
70
+ if line =~ /^\?\?\s(\w+)\//
71
+ folder = line.split("\s").last
72
+ message = "Sharing resolution: #{folder}."
73
+ rerere.cmd("add", folder)
74
+ rerere.cmd("commit", "-m", message)
75
+ rerere.cmd("push", "--quiet", opts.remote, opts.branch)
76
+ puts message
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ puts "POST-CHECKOUT.rb"
4
+ # Pull latest conflict resolutions.
5
+ `git share-rerere pull`
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ parents = `git rev-list -n 1 --parents HEAD`.split("\s")
4
+
5
+ # Shift off the last commit.
6
+ last = parents.shift
7
+
8
+ if parents.length >= 2
9
+ `git share-rerere pull`
10
+ `git share-rerere push`
11
+ end
@@ -0,0 +1,118 @@
1
+ #
2
+ # From homebrew (https://raw.github.com/mxcl/homebrew/go).
3
+ #
4
+ class Tty
5
+ class <<self
6
+ def blue; bold 34; end
7
+ def white; bold 39; end
8
+ def red; underline 31; end
9
+ def yellow; underline 33 ; end
10
+ def reset; escape 0; end
11
+ def em; underline 39; end
12
+ def green; color 92 end
13
+ def gray; bold 30 end
14
+
15
+ def width
16
+ `/usr/bin/tput cols`.strip.to_i
17
+ end
18
+
19
+ private
20
+ def color n
21
+ escape "0;#{n}"
22
+ end
23
+ def bold n
24
+ escape "1;#{n}"
25
+ end
26
+ def underline n
27
+ escape "4;#{n}"
28
+ end
29
+ def escape n
30
+ "\033[#{n}m" if $stdout.tty?
31
+ end
32
+ end
33
+ end
34
+
35
+ def ohai title, *sput
36
+ title = title.to_s[0, Tty.width - 4] if $stdout.tty?
37
+ puts "#{Tty.blue}==>#{Tty.white} #{title}#{Tty.reset}"
38
+ puts sput unless sput.empty?
39
+ end
40
+
41
+ def oh1 title
42
+ title = title.to_s[0, Tty.width - 4] if $stdout.tty?
43
+ puts "#{Tty.green}==>#{Tty.white} #{title}#{Tty.reset}"
44
+ end
45
+
46
+ def opoo warning
47
+ puts "#{Tty.red}Warning#{Tty.reset}: #{warning}"
48
+ end
49
+
50
+ def onoe error
51
+ lines = error.to_s.split'\n'
52
+ puts "#{Tty.red}Error#{Tty.reset}: #{lines.shift}"
53
+ puts lines unless lines.empty?
54
+ end
55
+
56
+ class Array
57
+ def shell_s
58
+ cp = dup
59
+ first = cp.shift
60
+ cp.map{ |arg| arg.gsub " ", "\\ " }.unshift(first) * " "
61
+ end
62
+
63
+ def shell_list
64
+ cp = dup
65
+ dup.map{ |val| " - #{val}" }.join("\n")
66
+ end
67
+ end
68
+
69
+ class String
70
+ def undent
71
+ gsub(/^.{#{slice(/^ +/).length}}/, '')
72
+ end
73
+ end
74
+
75
+ module GitHelpersMixin
76
+ def context(work_tree, git_dir, *args)
77
+ # Git pull requires absolute paths when executed from outside of the
78
+ # repository's work tree.
79
+ params = [
80
+ "--git-dir=#{File.expand_path(git_dir)}",
81
+ "--work-tree=#{File.expand_path(work_tree)}"
82
+ ]
83
+ return params + args
84
+ end
85
+
86
+ def branchExists?(branch)
87
+ ref = (branch.include? "refs/heads/") ? branch : "refs/heads/#{branch}"
88
+ begin
89
+ git('show-ref', '--verify', '--quiet', ref)
90
+ rescue
91
+ return false
92
+ end
93
+ return true
94
+ end
95
+
96
+ def refExists?(ref)
97
+ begin
98
+ git('show-ref', '--tags', '--heads', ref)
99
+ rescue
100
+ return false
101
+ end
102
+ return true
103
+ end
104
+
105
+ def terminate(message = nil)
106
+ puts message if message != nil
107
+ throw :exit
108
+ end
109
+
110
+ def promptYN(message)
111
+ puts
112
+ puts "#{message} [y/N]"
113
+ unless STDIN.gets.chomp == 'y'
114
+ return false
115
+ end
116
+ return true
117
+ end
118
+ end
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env ruby
2
+ # Licensed to the Apache Software Foundation (ASF) under one or more
3
+ # contributor license agreements. See the NOTICE file distributed with this
4
+ # work for additional information regarding copyright ownership. The ASF
5
+ # licenses this file to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations under
15
+ # the License.
16
+
17
+ require 'optparse'
18
+ require 'ostruct'
19
+ require 'fileutils'
20
+
21
+ module GitFlow
22
+ extend self
23
+
24
+ attr_accessor :should_run, :trace, :program
25
+
26
+ self.program = 'gitflow'
27
+ self.should_run = true # should we run at exit?
28
+
29
+ HELP = <<-HELP
30
+
31
+ GitFlow is a tool to create custom git commands implemented in ruby.
32
+ It is generic enougth to be used on any git based project besides Apache Buildr.
33
+
34
+ OVERVIEW:
35
+
36
+ gitflow is intended to help developers with their daily git workflow,
37
+ performing repetitive git commands for them. It is implemented in
38
+ ruby so you can do anything, from invoking rake tasks to telling
39
+ people on twitter you are having trouble with their code :P.
40
+
41
+ To get help for a specific command use:
42
+ gitflow.rb help command
43
+ gitflow.rb command --help
44
+
45
+ For convenience you can create an alias to be execute using git.
46
+ The following example registers buildr-git.rb, which provides apache
47
+ svn and git synchronization commands:
48
+
49
+ git config alias.apache '!'"ruby $PWD/doc/scripts/buildr-git.rb"
50
+
51
+ After that you can use
52
+ git apache command --help
53
+
54
+ EXTENDING YOUR WORKFLOW:
55
+
56
+ You can create your own gitflow commands, to adapt your development
57
+ workflow.
58
+
59
+ Simply create a ruby script somewhere say ~/.buildr/gitflow.rb
60
+ And alias it in your local repo:
61
+
62
+ git config alias.flow '!'"ruby ~/.buildr/gitflow.rb"
63
+ git config alias.work '!'"ruby ~/.buildr/gitflow.rb my-flow sub-work"
64
+
65
+ A sample command would look like this.. (you may want to look at buildr-git.rb)
66
+
67
+ #!/usr/bin/env ruby
68
+ require /path/to/gitflow.rb
69
+
70
+ class MyCommand < GitFlow/'my-flow'
71
+
72
+ @help = "Summary to be displayed when listing commands"
73
+ @documentation = "Very long help that will be paged if necessary. (for --help)"
74
+
75
+ # takes an openstruct to place default values and option values.
76
+ # returns an array of arguments given to optparse.on
77
+ def options(opts)
78
+ opts.something = 'default'
79
+ [
80
+ ['--name NAME', lambda { |n| opts.name = n }],
81
+ ['--yes', lambda { |n| opts.yes = true }]
82
+ ]
83
+ end
84
+
85
+ # takes the opts openstruct after options have been parsed and
86
+ # an argv array with non-option arguments.
87
+ def execute(opts, argv)
88
+ # you can run another command using
89
+ run('other-command', '--using-this', 'arg')
90
+ some = git('config', '--get', 'some.property').chomp rescue nil
91
+ page { puts "This will be paged on terminal if needed" }
92
+ end
93
+
94
+ class SubCommand < MyCommand/'sub-work'
95
+ ... # implement a subcommand
96
+ end
97
+
98
+ end
99
+
100
+ You would then get help for your command with
101
+
102
+ git flow my-flow --help
103
+ git work --help
104
+
105
+ Using gitflow you can customize per-project git interface.
106
+
107
+ HELP
108
+
109
+ # Pager from http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
110
+ def pager
111
+ return if RUBY_PLATFORM =~ /win32/
112
+ return unless STDOUT.tty?
113
+
114
+ read, write = IO.pipe
115
+
116
+ unless Kernel.fork # Child process
117
+ STDOUT.reopen(write)
118
+ STDERR.reopen(write) if STDERR.tty?
119
+ read.close
120
+ write.close
121
+ return
122
+ end
123
+
124
+ # Parent process, become pager
125
+ STDIN.reopen(read)
126
+ read.close
127
+ write.close
128
+
129
+ ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
130
+
131
+ Kernel.select [STDIN] # Wait until we have input before we start the pager
132
+ pager = ENV['PAGER'] || 'less'
133
+ exec pager rescue exec '/bin/sh', '-c', pager
134
+ end
135
+
136
+ # Return a class to be extended in order to register a GitFlow command
137
+ # if command name is nil, it will be registered as the top level command.
138
+ # Classes implementing commands also provide this method, allowing for
139
+ # sub-command creation.
140
+ def /(command_name)
141
+ command_name = command_name.to_s unless command_name.nil?
142
+ cls = Class.new { include GitFlow::Mixin }
143
+ (class << cls; self; end).module_eval do
144
+ attr_accessor :help, :documentation, :command
145
+ define_method(:/) do |subcommand|
146
+ raise "Subcommand cannot be nil" unless subcommand
147
+ GitFlow/([command_name, subcommand].compact.join(' '))
148
+ end
149
+ define_method(:inherited) do |subclass|
150
+ subclass.command = command_name
151
+ GitFlow.commands[command_name] = subclass
152
+ end
153
+ end
154
+ cls
155
+ end
156
+
157
+ def commands
158
+ @commands ||= Hash.new
159
+ end
160
+
161
+ def optparse
162
+ optparse = opt = OptionParser.new
163
+ opt.separator ' '
164
+ opt.separator 'OPTIONS'
165
+ opt.separator ' '
166
+ opt.on('-h', '--help', 'Display this help') do
167
+ GitFlow.pager; puts opt; throw :exit
168
+ end
169
+ opt.on('--trace', 'Display traces') { GitFlow.trace = true }
170
+ optparse
171
+ end
172
+
173
+ def command(argv)
174
+ cmds = []
175
+ argv.each_with_index do |arg, i|
176
+ arg = argv[0..i].join(' ')
177
+ cmds << commands[arg] if commands.key?(arg)
178
+ end
179
+ cmds.last || commands[nil]
180
+ end
181
+
182
+ def run(*argv)
183
+ catch :exit do
184
+ command = self.command(argv).new
185
+ argv = argv[command.class.command.split.length..-1] if command.class.command
186
+ parser = optparse
187
+ parser.banner = "Usage: #{GitFlow.program} #{command.class.command} [options]"
188
+ options = OpenStruct.new
189
+ if command.respond_to?(:options)
190
+ command.options(options).each { |args| parser.on(*args) }
191
+ end
192
+ if command.class.documentation && command.class.documentation != ''
193
+ parser.separator ' '
194
+ parser.separator command.class.documentation.split(/\n/)
195
+ end
196
+ parser.parse!(argv)
197
+ command.execute(options, argv)
198
+ end
199
+ end
200
+
201
+ module Mixin
202
+ include FileUtils
203
+
204
+ # Override this method in your command class if it
205
+ # needs to parse command line options.
206
+ #
207
+ # This method takes an openstruct object as argument
208
+ # allowing you to store default values on it, and
209
+ # set option values.
210
+ #
211
+ # The return value must be an array of arguments
212
+ # given to optparse.on
213
+ def options(opt)
214
+ []
215
+ end
216
+
217
+ # Override this method in your command class to implement
218
+ # the command.
219
+ # First argument is the openstruct object after
220
+ # it has been populated by the option parser.
221
+ # Second argument is the array of non-option arguments.
222
+ def execute(opt, argv)
223
+ fail "#{self.class.command} not implemented"
224
+ end
225
+
226
+ # Run the command line given on argv
227
+ def run(*argv, &block)
228
+ GitFlow.run(*argv, &block)
229
+ end
230
+
231
+ # Yield paging the blocks output if necessary.
232
+ def page
233
+ GitFlow.pager
234
+ yield
235
+ end
236
+
237
+ def trace(*str)
238
+ STDERR.puts(*str) if GitFlow.trace
239
+ end
240
+
241
+ def git(*args)
242
+ cmd = 'git ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ')
243
+ trace cmd
244
+ `#{cmd}`.tap {
245
+ fail "GIT command `#{cmd}` failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
246
+ }
247
+ end
248
+
249
+ def sh(*args)
250
+ `#{args.join(' ')}`.tap {
251
+ fail "Shell command `#{args.join(' ')}` failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
252
+ }
253
+ end
254
+
255
+ def expand_path(path, dir=Dir.pwd)
256
+ File.expand_path(path, dir)
257
+ end
258
+ end
259
+
260
+ class NoSuchCommand < GitFlow/nil
261
+ @documentation = HELP
262
+
263
+ def execute(opts, argv)
264
+ page do
265
+ puts "Command not found: #{argv.join(' ').inspect}"
266
+ puts "Try `#{GitFlow.program} help` to obtain a list of commands."
267
+ end
268
+ end
269
+ end
270
+
271
+ class HelpCommand < GitFlow/:help
272
+ @help = "Display help for a command or show command list"
273
+ @documentation = "Displays help for the command given as argument"
274
+
275
+ def execute(opts, argv)
276
+ if argv.empty?
277
+ opt = GitFlow.optparse
278
+ opt.banner = "Usage: #{GitFlow.program} command [options]"
279
+ opt.separator ' '
280
+ opt.separator 'COMMANDS'
281
+ opt.separator ' '
282
+ commands = GitFlow.commands.map { |name, cls| [nil, name, cls.help] }.
283
+ sort_by { |a| a[1] || '' }
284
+ commands.each { |a| opt.separator("%-2s%-25s%s" % a) if a[1] }
285
+ opt.separator ' '
286
+ opt.separator 'You can also obtain help for any command giving it --help.'
287
+ page { puts opt }
288
+ else
289
+ run(*(argv + ['--help']))
290
+ end
291
+ end
292
+ end
293
+
294
+ end
295
+
296
+ at_exit { GitFlow.run(*ARGV) if GitFlow.should_run }
@@ -0,0 +1,104 @@
1
+ require 'git_bpf/lib/gitflow'
2
+ require 'git_bpf/lib/git-helpers'
3
+
4
+ #
5
+ #
6
+ #
7
+ class Repository
8
+ extend GitFlow::Mixin
9
+
10
+ include GitHelpersMixin
11
+
12
+ attr_accessor :ctx, :remote_name, :path, :git_dir
13
+
14
+ def initialize(path)
15
+ path = File.expand_path(path)
16
+ git_dir = File.join(path, '.git')
17
+
18
+ if not File.directory? git_dir
19
+ terminate "#{path} is not a git repository."
20
+ end
21
+
22
+ self.git_dir = git_dir
23
+ self.path = path
24
+ self.ctx = [
25
+ "--git-dir=#{File.expand_path(git_dir)}",
26
+ "--work-tree=#{File.expand_path(path)}"
27
+ ]
28
+
29
+ end
30
+
31
+ def fetch(remote)
32
+ self.cmd("fetch", "--quiet", remote)
33
+ end
34
+
35
+ def remoteUrl(name)
36
+ begin
37
+ config(false, "--get", "remote.#{name}.url").chomp
38
+ rescue
39
+ terminate "No remote named '#{name}' in repository: #{self.path}."
40
+ end
41
+ end
42
+
43
+ def cmd(*args)
44
+ self.class.git(*(self.ctx + args))
45
+ end
46
+
47
+ def config(local, *args)
48
+ return nil if args.empty?
49
+
50
+ command = ["config"]
51
+ command.push "--local" if local
52
+ command += args
53
+
54
+ cmd(*(self.ctx + command))
55
+ end
56
+
57
+ def head
58
+ begin
59
+ cmd("rev-parse", "--quiet", "--abbrev-ref", "--verify", "HEAD")
60
+ rescue
61
+ return ''
62
+ end
63
+ end
64
+
65
+ def ref?(ref)
66
+ begin
67
+ cmd('show-ref', '--tags', '--heads', ref)
68
+ rescue
69
+ return false
70
+ end
71
+ return true
72
+ end
73
+
74
+ def branch?(branch, remote = nil)
75
+ if remote != nil
76
+ ref = "refs/remotes/#{remote}/#{branch}"
77
+ else
78
+ ref = (branch.include? "refs/heads/") ? branch : "refs/heads/#{branch}"
79
+ end
80
+
81
+ begin
82
+ cmd('show-ref', '--verify', '--quiet', ref)
83
+ rescue
84
+ return false
85
+ end
86
+ return true
87
+ end
88
+
89
+ def self.clone(url, dest)
90
+ git('clone', url, dest)
91
+ Repository.new dest
92
+ end
93
+
94
+ def self.init(dir, *args)
95
+ ctx = [
96
+ "--git-dir=#{File.join(dir, '.git')}",
97
+ "--work-tree=#{dir}",
98
+ ]
99
+ command = ['init'] + args
100
+ git(*(ctx + command))
101
+ Repository.new dir
102
+ end
103
+ end
104
+
@@ -0,0 +1,3 @@
1
+ module GitBpf
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git_bpf
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - tnightingale
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2013-03-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bundler
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 3
30
+ version: "1.3"
31
+ type: :development
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
42
+ version: "0"
43
+ type: :development
44
+ version_requirements: *id002
45
+ description: A collection of commands to help with implementing the branch-per-feature git development workflow.
46
+ email:
47
+ - tom@affinitybridge.com
48
+ executables:
49
+ - _git-bpf
50
+ - git-bpf-init
51
+ extensions: []
52
+
53
+ extra_rdoc_files: []
54
+
55
+ files:
56
+ - .gitignore
57
+ - Gemfile
58
+ - LICENSE
59
+ - README.md
60
+ - Rakefile
61
+ - bin/_git-bpf
62
+ - bin/git-bpf-init
63
+ - git_bpf.gemspec
64
+ - lib/git-bpf.rb
65
+ - lib/git_bpf.rb
66
+ - lib/git_bpf/commands/init.rb
67
+ - lib/git_bpf/commands/recreate-branch.rb
68
+ - lib/git_bpf/commands/share-rerere-cache.rb
69
+ - lib/git_bpf/hooks/post-checkout.rb
70
+ - lib/git_bpf/hooks/post-merge.rb
71
+ - lib/git_bpf/lib/git-helpers.rb
72
+ - lib/git_bpf/lib/gitflow.rb
73
+ - lib/git_bpf/lib/repository.rb
74
+ - lib/git_bpf/version.rb
75
+ has_rdoc: true
76
+ homepage: https://github.com/affinitybridge/git-bpf
77
+ licenses:
78
+ - BSD
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.6
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Git branch-per-feature helper commands.
105
+ test_files: []
106
+