git-smart 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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