git_bpf 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +26 -0
- data/README.md +67 -0
- data/Rakefile +1 -0
- data/bin/_git-bpf +3 -0
- data/bin/git-bpf-init +6 -0
- data/git_bpf.gemspec +23 -0
- data/lib/git-bpf.rb +4 -0
- data/lib/git_bpf.rb +8 -0
- data/lib/git_bpf/commands/init.rb +146 -0
- data/lib/git_bpf/commands/recreate-branch.rb +179 -0
- data/lib/git_bpf/commands/share-rerere-cache.rb +82 -0
- data/lib/git_bpf/hooks/post-checkout.rb +5 -0
- data/lib/git_bpf/hooks/post-merge.rb +11 -0
- data/lib/git_bpf/lib/git-helpers.rb +118 -0
- data/lib/git_bpf/lib/gitflow.rb +296 -0
- data/lib/git_bpf/lib/repository.rb +104 -0
- data/lib/git_bpf/version.rb +3 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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/
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/_git-bpf
ADDED
data/bin/git-bpf-init
ADDED
data/git_bpf.gemspec
ADDED
@@ -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
|
data/lib/git-bpf.rb
ADDED
data/lib/git_bpf.rb
ADDED
@@ -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,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
|
+
|
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
|
+
|