git-smart 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+
3
+ gem 'colorize'
4
+
5
+ group :development do
6
+ gem "rspec", "~> 2.3.0"
7
+ gem "rcov", ">= 0"
8
+ gem 'ruby-debug'
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.5.2"
11
+ end
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,9 @@
1
+ class Array
2
+ def group_by(&blk)
3
+ Hash.new { |h,k| h[k] = [] }.tap do |hash|
4
+ each do |element|
5
+ hash[blk.call(element)] << element
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,8 @@
1
+ class Object
2
+ def tapp(prefix = nil, &block)
3
+ block ||= lambda {|x| x }
4
+ str = if block[self].is_a? String then block[self] else block[self].inspect end
5
+ puts [prefix, str].compact.join(": ")
6
+ self
7
+ end
8
+ end
data/lib/git-smart.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+
3
+ class GitSmart
4
+ end
5
+
6
+ %w[core_ext git-smart commands].each { |dir|
7
+ Dir.glob(File.join(File.dirname(__FILE__), dir, '**', '*.rb')) { |f| require f }
8
+ }
@@ -0,0 +1,2 @@
1
+ class GitSmart::RunFailed < Exception; end
2
+ class GitSmart::UnexpectedOutput < Exception; end
@@ -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
@@ -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