git-smart 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/git-smart.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{git-smart}
8
- s.version = "0.1.3"
8
+ s.version = "0.1.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Glen Maddern"]
12
- s.date = %q{2011-01-04}
12
+ s.date = %q{2011-01-05}
13
13
  s.default_executable = %q{git-smart}
14
14
  s.description = %q{Installs some additional 'smart' git commands, like `git smart-pull`.}
15
15
  s.email = %q{glenmaddern@gmail.com}
@@ -26,7 +26,11 @@ Gem::Specification.new do |s|
26
26
  "Rakefile",
27
27
  "VERSION",
28
28
  "bin/git-smart",
29
+ "docs/images/git-smart.png",
30
+ "docs/smart-merge.html",
31
+ "docs/smart-pull.html",
29
32
  "git-smart.gemspec",
33
+ "lib/commands/smart-merge.rb",
30
34
  "lib/commands/smart-pull.rb",
31
35
  "lib/core_ext/array.rb",
32
36
  "lib/core_ext/hash.rb",
@@ -37,6 +41,8 @@ Gem::Specification.new do |s|
37
41
  "lib/git-smart/git_repo.rb",
38
42
  "lib/git-smart/git_smart.rb",
39
43
  "lib/git-smart/safe_shell.rb",
44
+ "spec/smart-merge_failures_spec.rb",
45
+ "spec/smart-merge_spec.rb",
40
46
  "spec/smart-pull_spec.rb",
41
47
  "spec/spec_helper.rb"
42
48
  ]
@@ -46,6 +52,8 @@ Gem::Specification.new do |s|
46
52
  s.rubygems_version = %q{1.3.7}
47
53
  s.summary = %q{Add some smarts to your git workflow}
48
54
  s.test_files = [
55
+ "spec/smart-merge_failures_spec.rb",
56
+ "spec/smart-merge_spec.rb",
49
57
  "spec/smart-pull_spec.rb",
50
58
  "spec/spec_helper.rb"
51
59
  ]
@@ -60,12 +68,14 @@ Gem::Specification.new do |s|
60
68
  s.add_development_dependency(%q<rcov>, [">= 0"])
61
69
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
62
70
  s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
71
+ s.add_development_dependency(%q<rocco>, [">= 0"])
63
72
  else
64
73
  s.add_dependency(%q<colorize>, [">= 0"])
65
74
  s.add_dependency(%q<rspec>, ["~> 2.3.0"])
66
75
  s.add_dependency(%q<rcov>, [">= 0"])
67
76
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
68
77
  s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
78
+ s.add_dependency(%q<rocco>, [">= 0"])
69
79
  end
70
80
  else
71
81
  s.add_dependency(%q<colorize>, [">= 0"])
@@ -73,6 +83,7 @@ Gem::Specification.new do |s|
73
83
  s.add_dependency(%q<rcov>, [">= 0"])
74
84
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
75
85
  s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
86
+ s.add_dependency(%q<rocco>, [">= 0"])
76
87
  end
77
88
  end
78
89
 
@@ -0,0 +1,63 @@
1
+ #Calling `git smart-merge branchname` will, quite simply, perform a
2
+ #non-fast-forward merge wrapped in a stash push/pop, if that's required.
3
+ #With some helpful extra output.
4
+ GitSmart.register 'smart-merge' do |repo, args|
5
+ #Let's begin!
6
+ current_branch = repo.current_branch
7
+ start "Starting: smart-merge on branch '#{current_branch}'"
8
+
9
+ #Grab the merge_target the user specified
10
+ merge_target = args.shift
11
+ failure "Usage: git smart-merge ref" if !merge_target
12
+
13
+ #Make sure git can resolve the reference to the merge_target
14
+ merge_sha = repo.sha(merge_target)
15
+ failure "Branch to merge '#{merge_target}' not recognised by git!" if !merge_sha
16
+
17
+ #If the SHA of HEAD and the merge_target are the same, we're trying to merge
18
+ #the same commit with itself. Which is madness!
19
+ head = repo.sha('HEAD')
20
+ if merge_sha == head
21
+ note "Branch '#{merge_target}' has no new commits. Nothing to merge in."
22
+ success 'Already up-to-date.'
23
+ else
24
+ #Determine the merge-base of the two commits, so we can report some useful output
25
+ #about how many new commits have been added.
26
+ merge_base = repo.merge_base(head, merge_sha)
27
+
28
+ #Report the number of commits on merge_target we're about to merge in.
29
+ new_commits_on_merge_target = repo.rev_list(merge_base, merge_target)
30
+ puts "Branch '#{merge_target}' has diverged by #{new_commits_on_merge_target.length} commit#{'s' if new_commits_on_merge_target.length != 1}. Merging in."
31
+
32
+ #Determine if our branch has moved on.
33
+ if head == merge_base
34
+ #Note: Even though we _can_ fast-forward here, it's a really bad idea since
35
+ #it results in the disappearance of the branch in history. For a good discussion
36
+ #on this topic, see this [StackOverflow question](http://stackoverflow.com/questions/2850369/why-uses-git-fast-forward-merging-per-default).
37
+ note "Branch '#{current_branch}' has not moved on since '#{merge_target}' diverged. Running with --no-ff anyway, since a fast-forward is unexpected behaviour."
38
+ else
39
+ #Report how many commits on our branch since merge_target diverged.
40
+ new_commits_on_branch = repo.rev_list(merge_base, head)
41
+ puts "Branch '#{current_branch}' has #{new_commits_on_branch.length} new commit#{'s' if new_commits_on_merge_target.length != 1} since '#{merge_target}' diverged."
42
+ end
43
+
44
+ #Before we merge, detect if there are local changes and stash them.
45
+ stash_required = repo.dirty?
46
+ if stash_required
47
+ note "Working directory dirty. Stashing..."
48
+ repo.stash!
49
+ end
50
+
51
+ #Perform the merge, using --no-ff.
52
+ repo.merge_no_ff!(merge_target)
53
+
54
+ #If we stashed before, pop now.
55
+ if stash_required
56
+ note "Reapplying local changes..."
57
+ repo.stash_pop!
58
+ end
59
+
60
+ #Display a nice completion message in large, friendly letters.
61
+ success "All good. Created merge commit #{repo.sha('HEAD')[0,7]}."
62
+ end
63
+ end
@@ -1,13 +1,22 @@
1
+ #Calling `git smart-pull` will fetch remote tracked changes
2
+ #and reapply your work on top of it. It's like a much, much
3
+ #smarter version of `git pull --rebase`.
4
+ #
5
+ #For some background as to why this is needed, see [my blog
6
+ #post about the perils of rebasing merge commits](https://gist.github.com/591209)
7
+ #
8
+ #This is how it works:
9
+
1
10
  GitSmart.register 'smart-pull' do |repo, args|
2
- # Let's begin!
11
+ #Let's begin!
3
12
  branch = repo.current_branch
4
13
  start "Starting: smart-pull on branch '#{branch}'"
5
14
 
6
- # Let's not have any arguments, fellas.
15
+ #Let's not have any arguments, fellas.
7
16
  warn "Ignoring arguments: #{args.inspect}" if !args.empty?
8
17
 
9
- # Try grabbing the tracking remote from the config. If it doesn't exist,
10
- # notify the user and choose 'origin'
18
+ #Try grabbing the tracking remote from the config. If it doesn't exist,
19
+ #notify the user and default to 'origin'
11
20
  tracking_remote = repo.tracking_remote ||
12
21
  note("No tracking remote configured, assuming 'origin'") ||
13
22
  'origin'
@@ -16,58 +25,77 @@ GitSmart.register 'smart-pull' do |repo, args|
16
25
  # but generally that's a good thing. This is the only communication we need to do with the server.
17
26
  repo.fetch!(tracking_remote)
18
27
 
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
28
+ #Try grabbing the tracking branch from the config. If it doesn't exist,
29
+ #notify the user and choose the branch of the same name
21
30
  tracking_branch = repo.tracking_branch ||
22
31
  note("No tracking branch configured, assuming '#{branch}'") ||
23
32
  branch
24
33
 
25
- # Check the upstream branch exists
34
+ #Check the specified upstream branch exists. Fail if it doesn't.
26
35
  upstream_branch = "#{tracking_remote}/#{tracking_branch}"
27
36
  failure("Upstream branch '#{upstream_branch}' doesn't exist!") if !repo.exists?(upstream_branch)
28
37
 
38
+ #Grab the SHAs of the commits we'll be working with.
29
39
  head = repo.sha('HEAD')
30
40
  remote = repo.sha(upstream_branch)
31
41
 
42
+ #If both HEAD and our upstream_branch resolve to the same SHA, there's nothing to do!
32
43
  if head == remote
33
44
  puts "Neither your local branch '#{branch}', nor the remote branch '#{upstream_branch}' have moved on."
34
45
  success "Already up-to-date"
35
46
  else
47
+ #Find out where the two branches diverged using merge-base. It's what git
48
+ #uses internally.
36
49
  merge_base = repo.merge_base(head, remote)
37
50
 
51
+ #By comparing the merge_base to both HEAD and the remote, we can
52
+ #determine whether both or only one have moved on.
53
+ #If the remote hasn't changed, we're already up to date, so there's nothing
54
+ #to pull.
38
55
  if merge_base == remote
39
56
  puts "Remote branch '#{upstream_branch}' has not moved on."
40
57
  success "Already up-to-date"
41
58
  else
59
+ #If the remote _has_ moved on, we actually have some work to do:
60
+ #
61
+ #First, report how many commits are new on remote. Because that's useful information.
42
62
  new_commits_on_remote = repo.rev_list(merge_base, remote)
43
63
  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."
64
+ note "There #{is_are} #{new_commits_on_remote.length} new commit#{s_or_not} on '#{upstream_branch}'."
45
65
 
66
+ #Next, detect if there are local changes and stash them.
46
67
  stash_required = repo.dirty?
47
68
  if stash_required
48
69
  note "Working directory dirty. Stashing..."
49
70
  repo.stash!
50
- else
51
- puts "No uncommitted changes, no need to stash."
52
71
  end
53
72
 
54
73
  success_messages = []
55
74
 
75
+ #Then, bring the local branch up to date.
76
+ #
77
+ #If our local branch hasn't moved on, that's easy - we just need to fast-forward.
56
78
  if merge_base == head
57
79
  puts "Local branch '#{branch}' has not moved on. Fast-forwarding..."
58
80
  repo.fast_forward!(upstream_branch)
59
81
  success_messages << "Fast forwarded from #{head[0,7]} to #{remote[0,7]}"
60
82
  else
83
+ #If our local branch has new commits, we need to rebase them on top of master.
84
+ #
85
+ #When we rebase, we use `git rebase -p`, which attempts to recreate merges
86
+ #instead of ignoring them. For a description as to why, see my [blog post]((https://gist.github.com/591209).
61
87
  note "Both local and remote branches have moved on. Branch 'master' needs to be rebased onto 'origin/master'"
62
88
  repo.rebase_preserving_merges!(upstream_branch)
63
89
  success_messages << "HEAD moved from #{head[0,7]} to #{repo.sha('HEAD')[0,7]}."
64
90
  end
65
91
 
92
+ #If we stashed before, pop now.
66
93
  if stash_required
67
94
  note "Reapplying local changes..."
68
95
  repo.stash_pop!
69
96
  end
70
97
 
98
+ #Display a nice completion message in large, friendly letters.
71
99
  success ["All good.", *success_messages].join(" ")
72
100
  end
73
101
 
@@ -22,7 +22,9 @@ class GitRepo
22
22
  def tracking_branch
23
23
  key = "branch.#{current_branch}.merge"
24
24
  value = config(key)
25
- if value =~ /^refs\/heads\/(.*)$/
25
+ if value.nil?
26
+ value
27
+ elsif value =~ /^refs\/heads\/(.*)$/
26
28
  $1
27
29
  else
28
30
  raise GitSmart::UnexpectedOutput.new("Expected the config of '#{key}' to be /refs/heads/branchname, got '#{value}'")
@@ -30,7 +32,7 @@ class GitRepo
30
32
  end
31
33
 
32
34
  def fetch!(remote)
33
- log_git('fetch', remote)
35
+ git!('fetch', remote)
34
36
  end
35
37
 
36
38
  def merge_base(ref_a, ref_b)
@@ -61,6 +63,7 @@ class GitRepo
61
63
  when /^M/; :modified
62
64
  when /^A/; :added
63
65
  when /^\?\?/; :untracked
66
+ when /^UU/; :conflicted
64
67
  else raise GitSmart::UnexpectedOutput.new("Expected the output of git status to only have lines starting with A,M, or ??. Got: \n#{raw_status}")
65
68
  end
66
69
  }
@@ -71,19 +74,19 @@ class GitRepo
71
74
  end
72
75
 
73
76
  def fast_forward!(upstream)
74
- log_git('merge', '--ff-only', upstream)
77
+ git!('merge', '--ff-only', upstream)
75
78
  end
76
79
 
77
80
  def stash!
78
- log_git('stash')
81
+ git!('stash')
79
82
  end
80
83
 
81
84
  def stash_pop!
82
- log_git('stash', 'pop')
85
+ git!('stash', 'pop')
83
86
  end
84
87
 
85
88
  def rebase_preserving_merges!(upstream)
86
- log_git('rebase', '-p', upstream)
89
+ git!('rebase', '-p', upstream)
87
90
  end
88
91
 
89
92
  def log(nr)
@@ -94,16 +97,22 @@ class GitRepo
94
97
  log(nr).map(&:last)
95
98
  end
96
99
 
100
+ def merge_no_ff!(target)
101
+ git!('merge', '--no-ff', target)
102
+ end
103
+
97
104
  # helper methods, left public in case other commands want to use them directly
98
105
 
99
106
  def git(*args)
100
- Dir.chdir(@dir) { SafeShell.execute('git', *args) }
107
+ output = exec_git(*args)
108
+ $?.success? ? output : ''
101
109
  end
102
110
 
103
- def log_git(*args)
111
+ def git!(*args)
104
112
  puts "Executing: #{['git', *args].join(" ")}"
105
- output = git(*args)
106
- puts output.split("\n").map { |l| " #{l}" }
113
+ output = exec_git(*args)
114
+ to_display = output.split("\n").map { |l| " #{l}" }.join("\n")
115
+ $?.success? ? puts(to_display) : raise(GitSmart::UnexpectedOutput.new(to_display))
107
116
  output
108
117
  end
109
118
 
@@ -111,4 +120,12 @@ class GitRepo
111
120
  remote = git('config', name).chomp
112
121
  remote.empty? ? nil : remote
113
122
  end
123
+
124
+ private
125
+
126
+ def exec_git(*args)
127
+ Dir.chdir(@dir) {
128
+ SafeShell.execute('git', *args)
129
+ }
130
+ end
114
131
  end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ require 'fileutils'
4
+
5
+ describe 'smart-merge with failures' do
6
+ def local_dir; WORKING_DIR + '/local'; end
7
+
8
+ before :each do
9
+ %x[
10
+ cd #{WORKING_DIR}
11
+ mkdir local
12
+ cd local
13
+ git init
14
+ echo -e 'one\ntwo\nthree\nfour\n' > README
15
+ mkdir lib
16
+ echo 'puts "pro hax"' > lib/codes.rb
17
+ git add .
18
+ git commit -m 'first'
19
+ ]
20
+ end
21
+
22
+ context "with conflicting changes on master and newbranch" do
23
+ before :each do
24
+ %x[
25
+ cd #{local_dir}
26
+ git checkout -b newbranch 2> /dev/null
27
+ echo 'one\nnewbranch changes\nfour\n' > README
28
+ git commit -am 'newbranch_commit'
29
+
30
+ git checkout master 2> /dev/null
31
+
32
+ echo 'one\nmaster changes\nfour\n' > README
33
+ git commit -am 'master_commit'
34
+ ]
35
+ end
36
+
37
+ it "should report the failure and give instructions to the user" do
38
+ out = run_command(local_dir, 'smart-merge', 'newbranch')
39
+ local_dir.should have_git_status(:conflicted => ['README'])
40
+ out.should_not report("All good")
41
+ out.should report("Executing: git merge --no-ff newbranch")
42
+ out.should report("CONFLICT (content): Merge conflict in README")
43
+ out.should report("Automatic merge failed; fix conflicts and then commit the result.")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,101 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ require 'fileutils'
4
+
5
+ describe 'smart-merge' do
6
+ def local_dir; WORKING_DIR + '/local'; end
7
+
8
+ before :each do
9
+ %x[
10
+ cd #{WORKING_DIR}
11
+ mkdir local
12
+ cd local
13
+ git init
14
+ echo 'hurr durr' > README
15
+ mkdir lib
16
+ echo 'puts "pro hax"' > lib/codes.rb
17
+ git add .
18
+ git commit -m 'first'
19
+ ]
20
+ end
21
+
22
+ it "should require an argument" do
23
+ out = run_command(local_dir, 'smart-merge')
24
+ out.should report("Usage: git smart-merge ref")
25
+ end
26
+
27
+ it "should require a valid branch" do
28
+ out = run_command(local_dir, 'smart-merge', 'foo')
29
+ out.should report("Branch to merge 'foo' not recognised by git!")
30
+ end
31
+
32
+ it "should report nothing to do if the branch hasn't moved on" do
33
+ %x[
34
+ cd #{local_dir}
35
+ git branch unmoved
36
+ ]
37
+ out = run_command(local_dir, 'smart-merge', 'unmoved')
38
+ out.should report("Branch 'unmoved' has no new commits. Nothing to merge in.")
39
+ out.should report("Already up-to-date.")
40
+ end
41
+
42
+ context "with local changes to newbranch" do
43
+ before :each do
44
+ %x[
45
+ cd #{local_dir}
46
+ git checkout -b newbranch 2> /dev/null
47
+ echo 'moar things!' >> README
48
+ echo 'puts "moar code!"' >> lib/moar.rb
49
+ git add .
50
+ git commit -m 'moar'
51
+
52
+ git checkout master 2> /dev/null
53
+ ]
54
+ end
55
+
56
+ it "should merge --no-ff, despite the branch being fast-forwardable" do
57
+ out = run_command(local_dir, 'smart-merge', 'newbranch')
58
+ out.should report("Branch 'newbranch' has diverged by 1 commit. Merging in.")
59
+ out.should report("* Branch 'master' has not moved on since 'newbranch' diverged. Running with --no-ff anyway, since a fast-forward is unexpected behaviour.")
60
+ out.should report("Executing: git merge --no-ff newbranch")
61
+ out.should report("2 files changed, 2 insertions(+), 0 deletions(-)")
62
+ out.should report(/All good\. Created merge commit [\w]{7}\./)
63
+ end
64
+
65
+ context "and changes on master" do
66
+ before :each do
67
+ %x[
68
+ cd #{local_dir}
69
+ echo "puts 'moar codes too!'" >> lib/codes.rb
70
+ git add .
71
+ git commit -m 'changes on master'
72
+ ]
73
+ end
74
+
75
+ it "should merge in ok" do
76
+ out = run_command(local_dir, 'smart-merge', 'newbranch')
77
+ out.should report("Branch 'newbranch' has diverged by 1 commit. Merging in.")
78
+ out.should report("Branch 'master' has 1 new commit since 'newbranch' diverged.")
79
+ out.should report("Executing: git merge --no-ff newbranch")
80
+ out.should report("2 files changed, 2 insertions(+), 0 deletions(-)")
81
+ out.should report(/All good\. Created merge commit [\w]{7}\./)
82
+ end
83
+
84
+ it "should stash then merge if working tree is dirty" do
85
+ %x[
86
+ cd #{local_dir}
87
+ echo "i am nub" > noob
88
+ echo "puts 'moar codes too!'" >> lib/codes.rb
89
+ git add noob
90
+ ]
91
+ out = run_command(local_dir, 'smart-merge', 'newbranch')
92
+ out.should report("Executing: git stash")
93
+ out.should report("Executing: git merge --no-ff newbranch")
94
+ out.should report("2 files changed, 2 insertions(+), 0 deletions(-)")
95
+ out.should report("Reapplying local changes...")
96
+ out.should report("Executing: git stash pop")
97
+ out.should report(/All good\. Created merge commit [\w]{7}\./)
98
+ end
99
+ end
100
+ end
101
+ end