git-smart 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +11 -0
- data/Gemfile.lock +38 -0
- data/LICENSE.txt +20 -0
- data/README.md +44 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/git-smart +59 -0
- data/lib/commands/smart-pull.rb +69 -0
- data/lib/core_ext/array.rb +9 -0
- data/lib/core_ext/hash.rb +21 -0
- data/lib/core_ext/object.rb +8 -0
- data/lib/git-smart.rb +8 -0
- data/lib/git-smart/exceptions.rb +2 -0
- data/lib/git-smart/execution_context.rb +39 -0
- data/lib/git-smart/git_repo.rb +111 -0
- data/lib/git-smart/git_smart.rb +25 -0
- data/lib/git-smart/safe_shell.rb +21 -0
- data/spec/smart-pull_spec.rb +161 -0
- data/spec/spec_helper.rb +64 -0
- metadata +176 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
colorize (0.5.8)
|
5
|
+
columnize (0.3.2)
|
6
|
+
diff-lcs (1.1.2)
|
7
|
+
git (1.2.5)
|
8
|
+
jeweler (1.5.2)
|
9
|
+
bundler (~> 1.0.0)
|
10
|
+
git (>= 1.2.5)
|
11
|
+
rake
|
12
|
+
linecache (0.43)
|
13
|
+
rake (0.8.7)
|
14
|
+
rcov (0.9.8)
|
15
|
+
rspec (2.3.0)
|
16
|
+
rspec-core (~> 2.3.0)
|
17
|
+
rspec-expectations (~> 2.3.0)
|
18
|
+
rspec-mocks (~> 2.3.0)
|
19
|
+
rspec-core (2.3.1)
|
20
|
+
rspec-expectations (2.3.0)
|
21
|
+
diff-lcs (~> 1.1.2)
|
22
|
+
rspec-mocks (2.3.0)
|
23
|
+
ruby-debug (0.10.4)
|
24
|
+
columnize (>= 0.1)
|
25
|
+
ruby-debug-base (~> 0.10.4.0)
|
26
|
+
ruby-debug-base (0.10.4)
|
27
|
+
linecache (>= 0.3)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
bundler (~> 1.0.0)
|
34
|
+
colorize
|
35
|
+
jeweler (~> 1.5.2)
|
36
|
+
rcov
|
37
|
+
rspec (~> 2.3.0)
|
38
|
+
ruby-debug
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Glen Maddern
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# git-smart
|
2
|
+
|
3
|
+
Adds some additional git commands to add some smarts to your workflow. These commands follow a few guidelines:
|
4
|
+
|
5
|
+
0. It should do the 'right thing' in all situations - an inexperienced git user should be guided away from making simple mistakes.
|
6
|
+
0. It should make every attempt to explain to the user what decisions it has made, and why.
|
7
|
+
0. All git commands that modify the repository should be shown to the user - hopefully this helps the user eventually learn the underlying git commands, and when they're relevant.
|
8
|
+
0. All git commands, destructive or not, and their output should be shown to the user with the -v/--verbose flag.
|
9
|
+
|
10
|
+
## git-smart-pull
|
11
|
+
|
12
|
+
Calling 'git smart-pull' will fetch remote tracked changes and reapply your work on top of it. It's like a much, much smarter version of 'git pull --rebase'.
|
13
|
+
|
14
|
+
For some background as to why this is needed, see [my blog post about the perils of rebasing merge commits](https://gist.github.com/591209)
|
15
|
+
|
16
|
+
This is how it works:
|
17
|
+
|
18
|
+
0. First, determine which remote branch to update from. Use branch tracking config if present, otherwise default to a remote of 'origin' and the same branch name. E.g. 'branchX', by default, tracks 'origin/branchX'.
|
19
|
+
0. Fetch the remote.
|
20
|
+
0. Determine what needs to be done:
|
21
|
+
- If the remote is a parent of HEAD, there's nothing to do.
|
22
|
+
- If HEAD is a parent of the remote, you simply need to reapply any working changes to the new HEAD. Stash, fast-forward, stash pop.
|
23
|
+
- If HEAD and the remote have diverged:
|
24
|
+
0. stash
|
25
|
+
0. rebase -p onto the remote
|
26
|
+
0. stash pop
|
27
|
+
0. update ORIG\_HEAD to be the previous local HEAD, as expected (rebase -p doesn't set ORIG\_HEAD correctly)
|
28
|
+
0. Output a summary of what just happened, as well as any new or updated branches that came down with the last fetch.
|
29
|
+
0. Output a big, green GIT UP GOOD or red GIT UP BAD, depending on how things went.
|
30
|
+
|
31
|
+
# Contributing to git-smart
|
32
|
+
|
33
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
34
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
35
|
+
* Fork the project
|
36
|
+
* Start a feature/bugfix branch
|
37
|
+
* Commit and push until you are happy with your contribution
|
38
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
39
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
40
|
+
|
41
|
+
# Copyright
|
42
|
+
|
43
|
+
Copyright (c) 2011 Glen Maddern. See LICENSE.txt for
|
44
|
+
further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "git-smart"
|
16
|
+
gem.homepage = "http://github.com/geelen/git-smart"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{Add some smarts to your git workflow}
|
19
|
+
gem.description = %Q{Installs some additional 'smart' git commands, like `git smart-pull`.}
|
20
|
+
gem.email = "glenmaddern@gmail.com"
|
21
|
+
gem.authors = ["Glen Maddern"]
|
22
|
+
end
|
23
|
+
Jeweler::RubygemsDotOrgTasks.new
|
24
|
+
|
25
|
+
require 'rspec/core'
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
28
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "git-smart #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/git-smart
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'trollop'
|
4
|
+
|
5
|
+
class GitSmartBinary
|
6
|
+
USAGE = %Q{Usage:
|
7
|
+
git-smart list List all the git commands that can be installed
|
8
|
+
git-smart install --all Install all git commands
|
9
|
+
git-smart install command [command] Install these git commands
|
10
|
+
}
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
# If this was called through a symlink, follow it and include the lib/git-smart.rb file
|
14
|
+
require File.expand_path(
|
15
|
+
File.join(
|
16
|
+
File.dirname(File.expand_path(
|
17
|
+
File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
18
|
+
)),
|
19
|
+
'../lib/git-smart'
|
20
|
+
)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def list
|
25
|
+
puts "These commands available to install:"
|
26
|
+
puts GitSmart.commands.keys.map { |c| " git-smart install " + c }.join("\n")
|
27
|
+
puts "\nOr install everything with: git-smart install --all"
|
28
|
+
end
|
29
|
+
|
30
|
+
def install(args)
|
31
|
+
cmds = if args.include? '--all'
|
32
|
+
GitSmart.commands.keys
|
33
|
+
else
|
34
|
+
args
|
35
|
+
end
|
36
|
+
install_dir = File.dirname(`which git`)
|
37
|
+
puts "Installing #{cmds.join(', ')} to #{install_dir}:"
|
38
|
+
cmds.each { |cmd|
|
39
|
+
filename = "#{install_dir}/git-#{cmd}"
|
40
|
+
File.open(filename, 'w') { |out|
|
41
|
+
out.puts %Q{#!/usr/bin/env ruby
|
42
|
+
|
43
|
+
require 'rubygems'
|
44
|
+
require 'git-smart'
|
45
|
+
|
46
|
+
GitSmart.run(#{cmd}, ARGV)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
`chmod a+x #{filename}`
|
50
|
+
puts "Wrote #{filename}"
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
case ARGV.shift
|
56
|
+
when 'list': GitSmartBinary.new.list
|
57
|
+
when 'install': GitSmartBinary.new.install(ARGV)
|
58
|
+
else puts GitSmartBinary::USAGE
|
59
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
GitSmart.register 'smart-pull' do |repo, args|
|
2
|
+
# Let's begin!
|
3
|
+
branch = repo.current_branch
|
4
|
+
start "Starting: smart-pull on branch '#{branch}'"
|
5
|
+
|
6
|
+
# Let's not have any arguments, fellas.
|
7
|
+
warn "Ignoring arguments: #{args.inspect}" if !args.empty?
|
8
|
+
|
9
|
+
# Try grabbing the tracking remote from the config. If it doesn't exist,
|
10
|
+
# notify the user and choose 'origin'
|
11
|
+
tracking_remote = repo.tracking_remote ||
|
12
|
+
note("No tracking remote configured, assuming 'origin'") ||
|
13
|
+
'origin'
|
14
|
+
|
15
|
+
# Fetch the remote. This pulls down all new commits from the server, not just our branch,
|
16
|
+
# but generally that's a good thing. This is the only communication we need to do with the server.
|
17
|
+
puts_with_done("Fetching '#{tracking_remote}'") { repo.fetch(tracking_remote) }
|
18
|
+
|
19
|
+
# Try grabbing the tracking branch from the config. If it doesn't exist,
|
20
|
+
# notify the user and choose the branch of the same name
|
21
|
+
tracking_branch = repo.tracking_branch ||
|
22
|
+
note("No tracking branch configured, assuming '#{branch}'") ||
|
23
|
+
branch
|
24
|
+
|
25
|
+
# Check the upstream branch exists
|
26
|
+
upstream_branch = "#{tracking_remote}/#{tracking_branch}"
|
27
|
+
failure("Upstream branch '#{upstream_branch}' doesn't exist!") if !repo.exists?(upstream_branch)
|
28
|
+
|
29
|
+
head = repo.sha('HEAD')
|
30
|
+
remote = repo.sha(upstream_branch)
|
31
|
+
|
32
|
+
if head == remote
|
33
|
+
puts "Neither your local branch '#{branch}', nor the remote branch '#{upstream_branch}' have moved on."
|
34
|
+
success "Already up-to-date"
|
35
|
+
else
|
36
|
+
merge_base = repo.merge_base(head, remote)
|
37
|
+
|
38
|
+
if merge_base == remote
|
39
|
+
puts "Remote branch '#{upstream_branch}' has not moved on."
|
40
|
+
success "Already up-to-date"
|
41
|
+
else
|
42
|
+
new_commits_on_remote = repo.rev_list(merge_base, remote)
|
43
|
+
is_are, s_or_not = (new_commits_on_remote.length == 1) ? ['is', ''] : ['are', 's']
|
44
|
+
note "There #{is_are} #{new_commits_on_remote.length} new commit#{s_or_not} on master."
|
45
|
+
|
46
|
+
stash_required = repo.dirty?
|
47
|
+
if stash_required
|
48
|
+
note "Working directory dirty. Stashing..."
|
49
|
+
repo.stash!
|
50
|
+
else
|
51
|
+
puts "No uncommitted changes, no need to stash."
|
52
|
+
end
|
53
|
+
|
54
|
+
if merge_base == head
|
55
|
+
puts "Local branch '#{branch}' has not moved on. Fast-forwarding..."
|
56
|
+
repo.fast_forward!(upstream_branch)
|
57
|
+
else
|
58
|
+
note "Both local and remote branches have moved on. Branch 'master' needs to be rebased onto 'origin/master'"
|
59
|
+
repo.rebase_preserving_merges!(upstream_branch)
|
60
|
+
end
|
61
|
+
|
62
|
+
if stash_required
|
63
|
+
note "Reapplying local changes..."
|
64
|
+
repo.stash_pop!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Hash
|
2
|
+
def map_keys &blk
|
3
|
+
map_keys_with_values { |k,v| blk.call(k) }
|
4
|
+
end
|
5
|
+
|
6
|
+
def map_keys_with_values &blk
|
7
|
+
result = {}
|
8
|
+
each { |k,v| result[blk.call(k,v)] = v}
|
9
|
+
result
|
10
|
+
end
|
11
|
+
|
12
|
+
def map_values &blk
|
13
|
+
map_values_with_keys { |k,v| blk.call(v) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def map_values_with_keys &blk
|
17
|
+
result = {}
|
18
|
+
each { |k,v| result[k] = blk.call(k,v)}
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
data/lib/git-smart.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# The context that commands get executed within. Used for defining and scoping helper methods.
|
2
|
+
|
3
|
+
class ExecutionContext
|
4
|
+
def initialize
|
5
|
+
require 'colorize'
|
6
|
+
end
|
7
|
+
|
8
|
+
def start msg
|
9
|
+
puts "- #{msg} -".green
|
10
|
+
end
|
11
|
+
|
12
|
+
def note msg
|
13
|
+
puts "* #{msg}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def warn msg
|
17
|
+
puts msg.red
|
18
|
+
end
|
19
|
+
|
20
|
+
def puts_with_done msg, &blk
|
21
|
+
print "#{msg}..."
|
22
|
+
blk.call
|
23
|
+
puts "done."
|
24
|
+
end
|
25
|
+
|
26
|
+
def success msg
|
27
|
+
puts big_message(msg).green
|
28
|
+
end
|
29
|
+
|
30
|
+
def failure msg
|
31
|
+
puts big_message(msg).red
|
32
|
+
raise GitSmart::RunFailed
|
33
|
+
end
|
34
|
+
|
35
|
+
def big_message msg
|
36
|
+
spacer_line = (" " + "-" * (msg.length + 20) + " ")
|
37
|
+
[spacer_line, "|" + " " * 10 + msg + " " * 10 + "|", spacer_line].join("\n")
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class GitRepo
|
2
|
+
def initialize(dir)
|
3
|
+
@dir = dir
|
4
|
+
end
|
5
|
+
|
6
|
+
def current_branch
|
7
|
+
File.read(File.join(@dir, ".git", "HEAD")).strip.sub(/^.*refs\/heads\//, '')
|
8
|
+
end
|
9
|
+
|
10
|
+
def sha(ref)
|
11
|
+
sha = git('rev-parse', ref).chomp
|
12
|
+
sha.empty? ? nil : sha
|
13
|
+
end
|
14
|
+
|
15
|
+
def tracking_remote
|
16
|
+
config("branch.#{current_branch}.remote")
|
17
|
+
end
|
18
|
+
|
19
|
+
def tracking_branch
|
20
|
+
key = "branch.#{current_branch}.merge"
|
21
|
+
value = config(key)
|
22
|
+
if value =~ /^refs\/heads\/(.*)$/
|
23
|
+
$1
|
24
|
+
else
|
25
|
+
raise GitSmart::UnexpectedOutput.new("Expected the config of '#{key}' to be /refs/heads/branchname, got '#{value}'")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch(remote)
|
30
|
+
git('fetch', remote)
|
31
|
+
end
|
32
|
+
|
33
|
+
def merge_base(ref_a, ref_b)
|
34
|
+
git('merge-base', ref_a, ref_b).chomp
|
35
|
+
end
|
36
|
+
|
37
|
+
def exists?(ref)
|
38
|
+
git('rev-parse', ref)
|
39
|
+
$?.success?
|
40
|
+
end
|
41
|
+
|
42
|
+
def rev_list(ref_a, ref_b)
|
43
|
+
git('rev-list', "#{ref_a}..#{ref_b}").split("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def raw_status
|
47
|
+
git('status', '-s')
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
raw_status.
|
52
|
+
split("\n").
|
53
|
+
map { |l| l.split(" ") }.
|
54
|
+
group_by(&:first).
|
55
|
+
map_values { |lines| lines.map(&:last) }.
|
56
|
+
map_keys { |status|
|
57
|
+
case status
|
58
|
+
when /^M/: :modified
|
59
|
+
when /^A/: :added
|
60
|
+
when /^\?\?/: :untracked
|
61
|
+
else raise GitSmart::UnexpectedOutput.new("Expected the output of git status to only have lines starting with A,M, or ??. Got: \n#{raw_status}")
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def dirty?
|
67
|
+
status.any? { |k,v| k != :untracked && v.any? }
|
68
|
+
end
|
69
|
+
|
70
|
+
def fast_forward!(upstream)
|
71
|
+
log_git('merge', '--ff-only', upstream)
|
72
|
+
end
|
73
|
+
|
74
|
+
def stash!
|
75
|
+
log_git('stash')
|
76
|
+
end
|
77
|
+
|
78
|
+
def stash_pop!
|
79
|
+
log_git('stash', 'pop')
|
80
|
+
end
|
81
|
+
|
82
|
+
def rebase_preserving_merges!(upstream)
|
83
|
+
log_git('rebase', '-p', upstream)
|
84
|
+
end
|
85
|
+
|
86
|
+
def log(nr)
|
87
|
+
git('log', '--oneline', '-n', nr.to_s).split("\n").map { |l| l.split(" ",2) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def log_commit_messages(nr)
|
91
|
+
log(nr).map(&:last)
|
92
|
+
end
|
93
|
+
|
94
|
+
# helper methods, left public in case other commands want to use them directly
|
95
|
+
|
96
|
+
def git(*args)
|
97
|
+
Dir.chdir(@dir) { SafeShell.execute('git', *args) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def log_git(*args)
|
101
|
+
puts "Executing: #{['git', *args].join(" ")}"
|
102
|
+
output = git(*args)
|
103
|
+
puts output.split("\n").map { |l| " #{l}" }
|
104
|
+
output
|
105
|
+
end
|
106
|
+
|
107
|
+
def config(name)
|
108
|
+
remote = git('config', name).chomp
|
109
|
+
remote.empty? ? nil : remote
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class GitSmart
|
2
|
+
def self.run(code, args)
|
3
|
+
lambda = commands[code]
|
4
|
+
if lambda
|
5
|
+
begin
|
6
|
+
lambda.call(args)
|
7
|
+
rescue GitSmart::RunFailed
|
8
|
+
end
|
9
|
+
else
|
10
|
+
puts "No command #{code.inspect} defined! Available commands are #{commands.keys.sort.inspect}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Used like this:
|
15
|
+
# GitSmart.register 'my-command' do |repo, args|
|
16
|
+
def self.register(code, &blk)
|
17
|
+
commands[code] = lambda { |args|
|
18
|
+
ExecutionContext.new.instance_exec(GitRepo.new("."), args, &blk)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.commands
|
23
|
+
@commands ||= {}
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SafeShell
|
2
|
+
def self.execute(command, *args)
|
3
|
+
read_end, write_end = IO.pipe
|
4
|
+
pid = fork do
|
5
|
+
read_end.close
|
6
|
+
STDOUT.reopen(write_end)
|
7
|
+
STDERR.reopen(write_end)
|
8
|
+
exec(command, *args)
|
9
|
+
end
|
10
|
+
write_end.close
|
11
|
+
output = read_end.read
|
12
|
+
Process.waitpid(pid)
|
13
|
+
read_end.close
|
14
|
+
output
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.execute?(*args)
|
18
|
+
execute(*args)
|
19
|
+
$?.success?
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
describe 'smart-pull' do
|
6
|
+
def local_dir; WORKING_DIR + '/local'; end
|
7
|
+
def remote_dir; WORKING_DIR + '/remote'; end
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
%x[
|
11
|
+
cd #{WORKING_DIR}
|
12
|
+
mkdir remote
|
13
|
+
cd remote
|
14
|
+
git init
|
15
|
+
echo 'hurr durr' > README
|
16
|
+
mkdir lib
|
17
|
+
echo 'puts "pro hax"' > lib/codes.rb
|
18
|
+
git add .
|
19
|
+
git commit -m 'first'
|
20
|
+
cd ..
|
21
|
+
|
22
|
+
git clone remote/.git local
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should tell us there's nothing to do" do
|
27
|
+
out = run_command(local_dir, 'smart-pull')
|
28
|
+
out.should report("Fetching 'origin'")
|
29
|
+
out.should report("Neither your local branch 'master', nor the remote branch 'origin/master' have moved on.")
|
30
|
+
out.should report("Already up-to-date")
|
31
|
+
end
|
32
|
+
|
33
|
+
context "with only local changes" do
|
34
|
+
before :each do
|
35
|
+
%x[
|
36
|
+
cd #{local_dir}
|
37
|
+
echo 'moar things!' >> README
|
38
|
+
echo 'puts "moar code!"' >> lib/moar.rb
|
39
|
+
git add .
|
40
|
+
git commit -m 'moar'
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should report that no remote changes were found" do
|
45
|
+
out = run_command(local_dir, 'smart-pull')
|
46
|
+
out.should report("Fetching 'origin'")
|
47
|
+
out.should report("Remote branch 'origin/master' has not moved on.")
|
48
|
+
out.should report("Already up-to-date")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with only remote changes" do
|
53
|
+
before :each do
|
54
|
+
%x[
|
55
|
+
cd #{remote_dir}
|
56
|
+
echo 'changed on the server!' >> README
|
57
|
+
git add .
|
58
|
+
git commit -m 'upstream changes'
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should fast-forward" do
|
63
|
+
out = run_command(local_dir, 'smart-pull')
|
64
|
+
out.should report("Fetching 'origin'")
|
65
|
+
out.should report("There is 1 new commit on master.")
|
66
|
+
out.should report("No uncommitted changes, no need to stash.")
|
67
|
+
out.should report("Local branch 'master' has not moved on. Fast-forwarding.")
|
68
|
+
out.should report("Executing: git merge --ff-only origin/master")
|
69
|
+
out.should report(/Updating [^\.]+..[^\.]+/)
|
70
|
+
out.should report("1 files changed, 1 insertions(+), 0 deletions(-)")
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should not stash before fast-forwarding if untracked files are present" do
|
74
|
+
%x[
|
75
|
+
cd #{local_dir}
|
76
|
+
echo "i am nub" > noob
|
77
|
+
]
|
78
|
+
local_dir.should have_git_status({:untracked => ['noob']})
|
79
|
+
out = run_command(local_dir, 'smart-pull')
|
80
|
+
out.should report("No uncommitted changes, no need to stash.")
|
81
|
+
out.should report("Executing: git merge --ff-only origin/master")
|
82
|
+
out.should report("1 files changed, 1 insertions(+), 0 deletions(-)")
|
83
|
+
local_dir.should have_git_status({:untracked => ['noob']})
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should stash, fast forward, pop if there are local changes" do
|
87
|
+
%x[
|
88
|
+
cd #{local_dir}
|
89
|
+
echo "i am nub" > noob
|
90
|
+
echo "puts 'moar codes too!'" >> lib/codes.rb
|
91
|
+
git add noob
|
92
|
+
]
|
93
|
+
local_dir.should have_git_status({:added => ['noob'], :modified => ['lib/codes.rb']})
|
94
|
+
out = run_command(local_dir, 'smart-pull')
|
95
|
+
out.should report("Working directory dirty. Stashing...")
|
96
|
+
out.should report("Executing: git stash")
|
97
|
+
out.should report("Executing: git merge --ff-only origin/master")
|
98
|
+
out.should report("1 files changed, 1 insertions(+), 0 deletions(-)")
|
99
|
+
out.should report("Reapplying local changes...")
|
100
|
+
out.should report("Executing: git stash pop")
|
101
|
+
local_dir.should have_git_status({:added => ['noob'], :modified => ['lib/codes.rb']})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "with diverged branches" do
|
106
|
+
before :each do
|
107
|
+
%x[
|
108
|
+
cd #{remote_dir}
|
109
|
+
echo 'changed on the server!' >> README
|
110
|
+
git add .
|
111
|
+
git commit -m 'upstream changes'
|
112
|
+
|
113
|
+
cd #{local_dir}
|
114
|
+
echo "puts 'moar codes too!'" >> lib/codes.rb
|
115
|
+
git add .
|
116
|
+
git commit -m 'local changes'
|
117
|
+
]
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should rebase" do
|
121
|
+
out = run_command(local_dir, 'smart-pull')
|
122
|
+
out.should report("Fetching 'origin'")
|
123
|
+
out.should report("There is 1 new commit on master.")
|
124
|
+
out.should report("Both local and remote branches have moved on. Branch 'master' needs to be rebased onto 'origin/master'")
|
125
|
+
out.should report("Executing: git rebase -p origin/master")
|
126
|
+
out.should report("Successfully rebased and updated refs/heads/master.")
|
127
|
+
local_dir.should have_last_few_commits(['local changes', 'upstream changes', 'first'])
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should not stash before rebasing if untracked files are present" do
|
131
|
+
%x[
|
132
|
+
cd #{local_dir}
|
133
|
+
echo "i am nub" > noob
|
134
|
+
]
|
135
|
+
local_dir.should have_git_status({:untracked => ['noob']})
|
136
|
+
out = run_command(local_dir, 'smart-pull')
|
137
|
+
out.should report("No uncommitted changes, no need to stash.")
|
138
|
+
out.should report("Executing: git rebase -p origin/master")
|
139
|
+
out.should report("Successfully rebased and updated refs/heads/master.")
|
140
|
+
local_dir.should have_git_status({:untracked => ['noob']})
|
141
|
+
local_dir.should have_last_few_commits(['local changes', 'upstream changes', 'first'])
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should stash, rebase, pop if there are local uncommitted changes" do
|
145
|
+
%x[
|
146
|
+
cd #{local_dir}
|
147
|
+
echo "i am nub" > noob
|
148
|
+
echo "puts 'moar codes too!'" >> lib/codes.rb
|
149
|
+
git add noob
|
150
|
+
]
|
151
|
+
local_dir.should have_git_status({:added => ['noob'], :modified => ['lib/codes.rb']})
|
152
|
+
out = run_command(local_dir, 'smart-pull')
|
153
|
+
out.should report("Working directory dirty. Stashing...")
|
154
|
+
out.should report("Executing: git stash")
|
155
|
+
out.should report("Executing: git rebase -p origin/master")
|
156
|
+
out.should report("Successfully rebased and updated refs/heads/master.")
|
157
|
+
local_dir.should have_git_status({:added => ['noob'], :modified => ['lib/codes.rb']})
|
158
|
+
local_dir.should have_last_few_commits(['local changes', 'upstream changes', 'first'])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'ruby-debug'
|
2
|
+
require 'rspec'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + '/../lib/git-smart'
|
5
|
+
|
6
|
+
WORKING_DIR = File.dirname(__FILE__) + '/working'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.before :each do
|
10
|
+
FileUtils.mkdir_p WORKING_DIR
|
11
|
+
end
|
12
|
+
|
13
|
+
config.after :each do
|
14
|
+
FileUtils.rm_rf WORKING_DIR
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_command(dir, command, args = [])
|
19
|
+
require 'stringio'
|
20
|
+
$stdout = stdout = StringIO.new
|
21
|
+
|
22
|
+
Dir.chdir(dir) { GitSmart.run(command, args) }
|
23
|
+
|
24
|
+
$stdout = STDOUT
|
25
|
+
stdout.string.split("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec::Matchers.define :report do |expected|
|
29
|
+
failure_message_for_should do |actual|
|
30
|
+
"expected to see #{expected.inspect}, got \n\n#{actual.map { |line| " #{line}" }.join("\n")}"
|
31
|
+
end
|
32
|
+
failure_message_for_should_not do |actual|
|
33
|
+
"expected not to see #{expected.inspect} in \n\n#{actual.map { |line| " #{line}" }.join("\n")}"
|
34
|
+
end
|
35
|
+
match do |actual|
|
36
|
+
actual.any? { |line| line[expected] }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
RSpec::Matchers.define :have_git_status do |expected|
|
42
|
+
failure_message_for_should do |dir|
|
43
|
+
"expected '#{dir}' to have git status of #{expected.inspect}, got #{GitRepo.new(dir).status.inspect}"
|
44
|
+
end
|
45
|
+
failure_message_for_should_not do |actual|
|
46
|
+
"expected '#{dir}' to not have git status of #{expected.inspect}, got #{GitRepo.new(dir).status.inspect}"
|
47
|
+
end
|
48
|
+
match do |dir|
|
49
|
+
GitRepo.new(dir).status == expected
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
RSpec::Matchers.define :have_last_few_commits do |expected|
|
55
|
+
failure_message_for_should do |dir|
|
56
|
+
"expected '#{dir}' to have last few commits of #{expected.inspect}, got #{GitRepo.new(dir).log_commit_messages(expected.length).inspect}"
|
57
|
+
end
|
58
|
+
failure_message_for_should_not do |actual|
|
59
|
+
"expected '#{dir}' to not have git status of #{expected.inspect}, got #{GitRepo.new(dir).log_commit_messages(expected.length).inspect}"
|
60
|
+
end
|
61
|
+
match do |dir|
|
62
|
+
GitRepo.new(dir).log_commit_messages(expected.length) == expected
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git-smart
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Glen Maddern
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-04 00:00:00 +11:00
|
19
|
+
default_executable: git-smart
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
prerelease: false
|
23
|
+
type: :runtime
|
24
|
+
name: colorize
|
25
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
requirement: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
prerelease: false
|
37
|
+
type: :development
|
38
|
+
name: rspec
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 3
|
48
|
+
- 0
|
49
|
+
version: 2.3.0
|
50
|
+
requirement: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
prerelease: false
|
53
|
+
type: :development
|
54
|
+
name: rcov
|
55
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirement: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
prerelease: false
|
67
|
+
type: :development
|
68
|
+
name: ruby-debug
|
69
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
requirement: *id004
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
prerelease: false
|
81
|
+
type: :development
|
82
|
+
name: bundler
|
83
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ~>
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 23
|
89
|
+
segments:
|
90
|
+
- 1
|
91
|
+
- 0
|
92
|
+
- 0
|
93
|
+
version: 1.0.0
|
94
|
+
requirement: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
prerelease: false
|
97
|
+
type: :development
|
98
|
+
name: jeweler
|
99
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ~>
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
hash: 7
|
105
|
+
segments:
|
106
|
+
- 1
|
107
|
+
- 5
|
108
|
+
- 2
|
109
|
+
version: 1.5.2
|
110
|
+
requirement: *id006
|
111
|
+
description: Installs some additional 'smart' git commands, like `git smart-pull`.
|
112
|
+
email: glenmaddern@gmail.com
|
113
|
+
executables:
|
114
|
+
- git-smart
|
115
|
+
extensions: []
|
116
|
+
|
117
|
+
extra_rdoc_files:
|
118
|
+
- LICENSE.txt
|
119
|
+
- README.md
|
120
|
+
files:
|
121
|
+
- Gemfile
|
122
|
+
- Gemfile.lock
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- VERSION
|
127
|
+
- bin/git-smart
|
128
|
+
- lib/commands/smart-pull.rb
|
129
|
+
- lib/core_ext/array.rb
|
130
|
+
- lib/core_ext/hash.rb
|
131
|
+
- lib/core_ext/object.rb
|
132
|
+
- lib/git-smart.rb
|
133
|
+
- lib/git-smart/exceptions.rb
|
134
|
+
- lib/git-smart/execution_context.rb
|
135
|
+
- lib/git-smart/git_repo.rb
|
136
|
+
- lib/git-smart/git_smart.rb
|
137
|
+
- lib/git-smart/safe_shell.rb
|
138
|
+
- spec/smart-pull_spec.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
has_rdoc: true
|
141
|
+
homepage: http://github.com/geelen/git-smart
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
hash: 3
|
155
|
+
segments:
|
156
|
+
- 0
|
157
|
+
version: "0"
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
hash: 3
|
164
|
+
segments:
|
165
|
+
- 0
|
166
|
+
version: "0"
|
167
|
+
requirements: []
|
168
|
+
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 1.3.7
|
171
|
+
signing_key:
|
172
|
+
specification_version: 3
|
173
|
+
summary: Add some smarts to your git workflow
|
174
|
+
test_files:
|
175
|
+
- spec/smart-pull_spec.rb
|
176
|
+
- spec/spec_helper.rb
|