grit 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grit might be problematic. Click here for more details.

@@ -209,6 +209,20 @@ module Grit
209
209
  Commit.find_all(self, id, options).first
210
210
  end
211
211
 
212
+ # Returns a list of commits that is in +other_repo+ but not in self
213
+ #
214
+ # Returns Grit::Commit[]
215
+ def commit_deltas_from(other_repo, ref = "master", other_ref = "master")
216
+ # TODO: we should be able to figure out the branch point, rather than
217
+ # rev-list'ing the whole thing
218
+ repo_refs = self.git.rev_list({}, ref).strip.split("\n")
219
+ other_repo_refs = other_repo.git.rev_list({}, other_ref).strip.split("\n")
220
+
221
+ (other_repo_refs - repo_refs).map do |ref|
222
+ Commit.find_all(other_repo, ref, {:max_count => 1}).first
223
+ end
224
+ end
225
+
212
226
  # The Tree object for the given treeish reference
213
227
  # +treeish+ is the reference (default 'master')
214
228
  # +paths+ is an optional Array of directory paths to restrict the tree (deafult [])
@@ -324,11 +338,19 @@ module Grit
324
338
  self.git.archive(options, treeish, "| gzip")
325
339
  end
326
340
 
327
- # run archive directly to a file
328
- def archive_to_file(treeish = 'master', prefix = nil, filename = 'archive.tar.gz')
341
+ # Write an archive directly to a file
342
+ # +treeish+ is the treeish name/id (default 'master')
343
+ # +prefix+ is the optional prefix (default nil)
344
+ # +filename+ is the name of the file (default 'archive.tar.gz')
345
+ # +format+ is the optional format (default nil)
346
+ # +pipe+ is the command to run the output through (default 'gzip')
347
+ #
348
+ # Returns nothing
349
+ def archive_to_file(treeish = 'master', prefix = nil, filename = 'archive.tar.gz', format = nil, pipe = "gzip")
329
350
  options = {}
330
351
  options[:prefix] = prefix if prefix
331
- self.git.archive(options, treeish, "| gzip > #{filename}")
352
+ options[:format] = format if format
353
+ self.git.archive(options, treeish, "| #{pipe} > #{filename}")
332
354
  end
333
355
 
334
356
  # Enable git-daemon serving of this repository by writing the
@@ -0,0 +1,7 @@
1
+ class String
2
+ if ((defined? RUBY_VERSION) && (RUBY_VERSION[0..2] == "1.9"))
3
+ def getord(offset); self[offset].ord; end
4
+ else
5
+ alias :getord :[]
6
+ end
7
+ end
@@ -54,7 +54,7 @@ module Grit
54
54
  blob = commit.tree/'.gitmodules'
55
55
  return {} unless blob
56
56
 
57
- lines = blob.data.split("\n")
57
+ lines = blob.data.gsub(/\r\n?/, "\n" ).split("\n")
58
58
 
59
59
  config = {}
60
60
  current = nil
@@ -75,6 +75,10 @@ module Grit
75
75
  config
76
76
  end
77
77
 
78
+ def basename
79
+ File.basename(name)
80
+ end
81
+
78
82
  # Pretty object inspection
79
83
  def inspect
80
84
  %Q{#<Grit::Submodule "#{@id}">}
@@ -1,71 +1,66 @@
1
1
  module Grit
2
-
3
- class Tag
4
- attr_reader :name
5
- attr_reader :commit
6
-
7
- # Instantiate a new Tag
8
- # +name+ is the name of the head
9
- # +commit+ is the Commit that the head points to
10
- #
11
- # Returns Grit::Tag (baked)
12
- def initialize(name, commit)
13
- @name = name
14
- @commit = commit
15
- end
16
-
17
- # Find all Tags
18
- # +repo+ is the Repo
19
- # +options+ is a Hash of options
20
- #
21
- # Returns Grit::Tag[] (baked)
2
+
3
+ class Tag < Ref
22
4
  def self.find_all(repo, options = {})
23
- default_options = {:sort => "committerdate",
24
- :format => "%(refname)%00%(objectname)"}
25
-
26
- actual_options = default_options.merge(options)
27
-
28
- output = repo.git.for_each_ref(actual_options, "refs/tags")
29
-
30
- self.list_from_string(repo, output)
31
- end
32
-
33
- # Parse out tag information into an array of baked Tag objects
34
- # +repo+ is the Repo
35
- # +text+ is the text output from the git command
36
- #
37
- # Returns Grit::Tag[] (baked)
38
- def self.list_from_string(repo, text)
39
- tags = []
40
-
41
- text.split("\n").each do |line|
42
- tags << self.from_string(repo, line)
5
+ refs = []
6
+ already = {}
7
+
8
+ Dir.chdir(repo.path) do
9
+ files = Dir.glob(prefix + '/**/*')
10
+
11
+ files.each do |ref|
12
+ next if !File.file?(ref)
13
+
14
+ id = File.read(ref).chomp
15
+ name = ref.sub("#{prefix}/", '')
16
+ commit = commit_from_sha(repo, id)
17
+
18
+ if !already[name]
19
+ refs << self.new(name, commit)
20
+ already[name] = true
21
+ end
22
+ end
23
+
24
+ if File.file?('packed-refs')
25
+ lines = File.readlines('packed-refs')
26
+ lines.each_with_index do |line, i|
27
+ if m = /^(\w{40}) (.*?)$/.match(line)
28
+ next if !Regexp.new('^' + prefix).match(m[2])
29
+ name = m[2].sub("#{prefix}/", '')
30
+
31
+ # Annotated tags in packed-refs include a reference
32
+ # to the commit object on the following line.
33
+ next_line = lines[i+1]
34
+ if next_line && next_line[0] == ?^
35
+ commit = Commit.create(repo, :id => next_line[1..-1].chomp)
36
+ else
37
+ commit = commit_from_sha(repo, m[1])
38
+ end
39
+
40
+ if !already[name]
41
+ refs << self.new(name, commit)
42
+ already[name] = true
43
+ end
44
+ end
45
+ end
46
+ end
43
47
  end
44
-
45
- tags
48
+
49
+ refs
46
50
  end
47
-
48
- # Create a new Tag instance from the given string.
49
- # +repo+ is the Repo
50
- # +line+ is the formatted tag information
51
- #
52
- # Format
53
- # name: [a-zA-Z_/]+
54
- # <null byte>
55
- # id: [0-9A-Fa-f]{40}
56
- #
57
- # Returns Grit::Tag (baked)
58
- def self.from_string(repo, line)
59
- full_name, id = line.split("\0")
60
- name = full_name.split("/").last
61
- commit = Commit.create(repo, :id => id)
62
- self.new(name, commit)
63
- end
64
-
65
- # Pretty object inspection
66
- def inspect
67
- %Q{#<Grit::Tag "#{@name}">}
51
+
52
+ def self.commit_from_sha(repo, id)
53
+ git_ruby_repo = GitRuby::Repository.new(repo.path)
54
+ object = git_ruby_repo.get_object_by_sha1(id)
55
+
56
+ if object.type == :commit
57
+ Commit.create(repo, :id => id)
58
+ elsif object.type == :tag
59
+ Commit.create(repo, :id => object.object)
60
+ else
61
+ raise "Unknown object type."
62
+ end
68
63
  end
69
- end # Tag
70
-
71
- end # Grit
64
+ end
65
+
66
+ end
@@ -95,10 +95,29 @@ module Grit
95
95
  end
96
96
  end
97
97
 
98
+ def basename
99
+ File.basename(name)
100
+ end
101
+
98
102
  # Pretty object inspection
99
103
  def inspect
100
104
  %Q{#<Grit::Tree "#{@id}">}
101
105
  end
106
+
107
+ # Find only Tree objects from contents
108
+ def trees
109
+ contents.select {|v| v.kind_of? Tree}
110
+ end
111
+
112
+ # Find only Blob objects from contents
113
+ def blobs
114
+ contents.select {|v| v.kind_of? Blob}
115
+ end
116
+
117
+ # Compares trees by name
118
+ def <=>(other)
119
+ name <=> other.name
120
+ end
102
121
  end # Tree
103
-
122
+
104
123
  end # Grit
@@ -0,0 +1 @@
1
+ x5�1� @QkN�0�� P8����^H����ޤ���x��hͿb�0Ml&m��H�$d��cy4β��;Bh�,�d3��&,�W��s_Vxp]���p���o)C���D�dȒ�#:D�n6cP?��0�
@@ -5,5 +5,6 @@ ca8a30f5a7f0f163bbe3b6f0abf18a6c83b0687a refs/heads/test/chacon
5
5
  2d3acf90f35989df8f262dc50beadc4ee3ae1560 refs/heads/testing
6
6
  ca8a30f5a7f0f163bbe3b6f0abf18a6c83b0687a refs/remotes/origin/master
7
7
  2d3acf90f35989df8f262dc50beadc4ee3ae1560 refs/remotes/tom/master
8
- f0055fda16c18fd8b27986dbf038c735b82198d7 refs/tags/v0.7.0
8
+ f0055fda16c18fd8b27986dbf038c735b82198d7 refs/tags/packed_annotated
9
9
  ^7bcc0ee821cdd133d8a53e8e7173a334fef448aa
10
+ ca8a30f5a7f0f163bbe3b6f0abf18a6c83b0687a refs/tags/packed
@@ -0,0 +1 @@
1
+ b7f932bd02b3e0a4228ee7b55832749028d345de
@@ -0,0 +1 @@
1
+ ca8a30f5a7f0f163bbe3b6f0abf18a6c83b0687a
@@ -0,0 +1,8 @@
1
+ e34590b7a2d186b3bb9a1170d02d52b36c791c78
2
+ 8977833d74f8681aa0d9a5e84b0dd3d81519774d
3
+ 6f5561530cb3a94e4c86454e84732197325be172
4
+ ee419e04a961543444be6db66aef52e6e37936d6
5
+ d845de9d438e1a249a0c2fcb778e8ea3b7e06cef
6
+ 0bba4a6c10060405a94d52533af2f9bdacd4f29c
7
+ 77711c0722964ead965e0ba2ee9ed4a03cb3d292
8
+ 501d23cac6dd911511f15d091ee031a15b90ebde
@@ -0,0 +1,11 @@
1
+ 4c8124ffcf4039d292442eeccabdeca5af5c5017
2
+ 634396b2f541a9f2d58b00be1a07f0c358b999b3
3
+ ab25fd8483882c3bda8a458ad2965d2248654335
4
+ e34590b7a2d186b3bb9a1170d02d52b36c791c78
5
+ 8977833d74f8681aa0d9a5e84b0dd3d81519774d
6
+ 6f5561530cb3a94e4c86454e84732197325be172
7
+ ee419e04a961543444be6db66aef52e6e37936d6
8
+ d845de9d438e1a249a0c2fcb778e8ea3b7e06cef
9
+ 0bba4a6c10060405a94d52533af2f9bdacd4f29c
10
+ 77711c0722964ead965e0ba2ee9ed4a03cb3d292
11
+ 501d23cac6dd911511f15d091ee031a15b90ebde
@@ -0,0 +1,637 @@
1
+ commit f8dd1f0b48e1106a62b47cc2927609ca589dc39a
2
+ tree 4c23a5137714e62c52f22e99b3104122868400ab
3
+ parent a0710955e70cbceef8cf805645a447f1b370b966
4
+ parent b724247612be9f55df7914cb86b64703810d7b73
5
+ author administrator <gmalamid@thoughtworks.com> 1224499981 +0100
6
+ committer administrator <gmalamid@thoughtworks.com> 1224499981 +0100
7
+
8
+ imported William Morgans utilities too, which are very very nice
9
+
10
+ diff --cc git-publish-branch
11
+ index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..b28b4fde621dff1c72bcb570183bbe923e4a24b2
12
+ new file mode 100755
13
+ --- /dev/null
14
+ +++ b/git-publish-branch
15
+ @@@ -1,0 -1,0 +1,70 @@@
16
+ ++#!/usr/bin/env ruby
17
+ ++
18
+ ++## git-publish-branch: a simple script to ease the unnecessarily complex
19
+ ++## task of "publishing" a branch, i.e., taking a local branch, creating a
20
+ ++## reference to it on a remote repo, and setting up the local branch to
21
+ ++## track the remote one, all in one go. you can even delete that remote
22
+ ++## reference.
23
+ ++##
24
+ ++## Usage: git publish-branch [-d] <branch> [repository]
25
+ ++##
26
+ ++## '-d' signifies deletion. <branch> is the branch to publish, and
27
+ ++## [repository] defaults to "origin". The remote branch name will be the
28
+ ++## same as the local branch name. Don't make life unnecessarily complex
29
+ ++## for yourself.
30
+ ++##
31
+ ++## Note that unpublishing a branch doesn't delete the local branch.
32
+ ++## Safety first!
33
+ ++##
34
+ ++## git-publish-branch Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
35
+ ++## This program is free software: you can redistribute it and/or modify
36
+ ++## it under the terms of the GNU General Public License as published by
37
+ ++## the Free Software Foundation, either version 3 of the License, or (at
38
+ ++## your option) any later version.
39
+ ++##
40
+ ++## This program is distributed in the hope that it will be useful,
41
+ ++## but WITHOUT ANY WARRANTY; without even the implied warranty of
42
+ ++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43
+ ++## GNU General Public License for more details.
44
+ ++##
45
+ ++## You can find the GNU General Public License at:
46
+ ++## http://www.gnu.org/licenses/
47
+ ++
48
+ ++def exec cmd
49
+ ++ puts cmd
50
+ ++ system cmd or die unless $fake
51
+ ++end
52
+ ++
53
+ ++def die s=nil
54
+ ++ $stderr.puts s if s
55
+ ++ exit(-1)
56
+ ++end
57
+ ++
58
+ ++head = `git symbolic-ref HEAD`.chomp.gsub(/refs\/heads\//, "")
59
+ ++delete = ARGV.delete "-d"
60
+ ++$fake = ARGV.delete "-n"
61
+ ++branch = (ARGV.shift || head).gsub(/refs\/heads\//, "")
62
+ ++remote = ARGV.shift || "origin"
63
+ ++local_ref = `git show-ref heads/#{branch}`
64
+ ++remote_ref = `git show-ref remotes/#{remote}/#{branch}`
65
+ ++remote_config = `git config branch.#{branch}.merge`
66
+ ++
67
+ ++if delete
68
+ ++ ## we don't do any checking here because the remote branch might actually
69
+ ++ ## exist, whether we actually know about it or not.
70
+ ++ exec "git push #{remote} :refs/heads/#{branch}"
71
+ ++
72
+ ++ unless local_ref.empty?
73
+ ++ exec "git config --unset branch.#{branch}.remote"
74
+ ++ exec "git config --unset branch.#{branch}.merge"
75
+ ++ end
76
+ ++else
77
+ ++ die "No local branch #{branch} exists!" if local_ref.empty?
78
+ ++ die "A remote branch #{branch} on #{remote} already exists!" unless remote_ref.empty?
79
+ ++ die "Local branch #{branch} is already a tracking branch!" unless remote_config.empty?
80
+ ++
81
+ ++ exec "git push #{remote} #{branch}:refs/heads/#{branch}"
82
+ ++ exec "git config branch.#{branch}.remote #{remote}"
83
+ ++ exec "git config branch.#{branch}.merge refs/heads/#{branch}"
84
+ ++end
85
+ ++
86
+ diff --cc git-rank-contributors
87
+ index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..3b272206d185e2f9b8089e6aaa00fde6acc308ef
88
+ new file mode 100755
89
+ --- /dev/null
90
+ +++ b/git-rank-contributors
91
+ @@@ -1,0 -1,0 +1,60 @@@
92
+ ++#!/usr/bin/env ruby
93
+ ++
94
+ ++## git-rank-contributors: a simple script to trace through the logs and
95
+ ++## rank contributors by the total size of the diffs they're responsible for.
96
+ ++## A change counts twice as much as a plain addition or deletion.
97
+ ++##
98
+ ++## Output may or may not be suitable for inclusion in a CREDITS file.
99
+ ++## Probably not without some editing, because people often commit from more
100
+ ++## than one address.
101
+ ++##
102
+ ++## git-rank-contributors Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
103
+ ++## This program is free software: you can redistribute it and/or modify
104
+ ++## it under the terms of the GNU General Public License as published by
105
+ ++## the Free Software Foundation, either version 3 of the License, or (at
106
+ ++## your option) any later version.
107
+ ++##
108
+ ++## This program is distributed in the hope that it will be useful,
109
+ ++## but WITHOUT ANY WARRANTY; without even the implied warranty of
110
+ ++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
111
+ ++## GNU General Public License for more details.
112
+ ++##
113
+ ++## You can find the GNU General Public License at:
114
+ ++## http://www.gnu.org/licenses/
115
+ ++
116
+ ++class String
117
+ ++ def obfuscate; gsub(/@/, " at the ").gsub(/\.(\w+)(>|$)/, ' dot \1s\2') end
118
+ ++ def htmlize; gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
119
+ ++end
120
+ ++
121
+ ++lines = {}
122
+ ++verbose = ARGV.delete("-v")
123
+ ++obfuscate = ARGV.delete("-o")
124
+ ++htmlize = ARGV.delete("-h")
125
+ ++
126
+ ++author = nil
127
+ ++state = :pre_author
128
+ ++`git log -M -C -C -p --no-color`.each do |l|
129
+ ++ case
130
+ ++ when (state == :pre_author || state == :post_author) && l =~ /Author: (.*)$/
131
+ ++ author = $1
132
+ ++ state = :post_author
133
+ ++ lines[author] ||= 0
134
+ ++ when state == :post_author && l =~ /^\+\+\+/
135
+ ++ state = :in_diff
136
+ ++ when state == :in_diff && l =~ /^[\+\-]/
137
+ ++ lines[author] += 1
138
+ ++ when state == :in_diff && l =~ /^commit /
139
+ ++ state = :pre_author
140
+ ++ end
141
+ ++end
142
+ ++
143
+ ++lines.sort_by { |a, c| -c }.each do |a, c|
144
+ ++ a = a.obfuscate if obfuscate
145
+ ++ a = a.htmlize if htmlize
146
+ ++ if verbose
147
+ ++ puts "#{a}: #{c} lines of diff"
148
+ ++ else
149
+ ++ puts a
150
+ ++ end
151
+ ++end
152
+ diff --cc git-show-merges
153
+ index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..12907502a6c6885e7183a0e71d9cfeed77917824
154
+ new file mode 100755
155
+ --- /dev/null
156
+ +++ b/git-show-merges
157
+ @@@ -1,0 -1,0 +1,49 @@@
158
+ ++#!/usr/bin/env ruby
159
+ ++
160
+ ++## git-show-merges: a simple script to show you which topic branches have
161
+ ++## been merged into the current branch, and which haven't. (Or, specify
162
+ ++## the set of merge branches you're interested in on the command line.)
163
+ ++##
164
+ ++## git-show-merges Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
165
+ ++## This program is free software: you can redistribute it and/or modify
166
+ ++## it under the terms of the GNU General Public License as published by
167
+ ++## the Free Software Foundation, either version 3 of the License, or (at
168
+ ++## your option) any later version.
169
+ ++##
170
+ ++## This program is distributed in the hope that it will be useful,
171
+ ++## but WITHOUT ANY WARRANTY; without even the implied warranty of
172
+ ++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
173
+ ++## GNU General Public License for more details.
174
+ ++##
175
+ ++## You can find the GNU General Public License at:
176
+ ++## http://www.gnu.org/licenses/
177
+ ++heads = if ARGV.empty?
178
+ ++ [`git symbolic-ref HEAD`.chomp]
179
+ ++else
180
+ ++ ARGV
181
+ ++end.map { |r| r.gsub(/refs\/heads\//, "") }
182
+ ++
183
+ ++branches = `git show-ref --heads`.
184
+ ++ scan(/^\S+ refs\/heads\/(\S+)$/).
185
+ ++ map { |a| a.first }
186
+ ++
187
+ ++unknown = heads - branches
188
+ ++unless unknown.empty?
189
+ ++ $stderr.puts "Unknown branch: #{unknown.first}"
190
+ ++ exit(-1)
191
+ ++end
192
+ ++
193
+ ++branches -= heads
194
+ ++
195
+ ++heads.each do |h|
196
+ ++ merged = branches.select { |b| `git log #{h}..#{b}` == "" }
197
+ ++ unmerged = branches - merged
198
+ ++
199
+ ++ puts "merged into #{h}:"
200
+ ++ merged.each { |b| puts " #{b}" }
201
+ ++ puts
202
+ ++ puts "not merged into #{h}: "
203
+ ++ unmerged.each { |b| puts " #{b}" }
204
+ ++
205
+ ++ puts
206
+ ++end
207
+ diff --cc git-wt-add
208
+ index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..3125ab5495e5f2ab55ed48af5303031fae02c2ac
209
+ new file mode 100755
210
+ --- /dev/null
211
+ +++ b/git-wt-add
212
+ @@@ -1,0 -1,0 +1,196 @@@
213
+ ++#!/usr/bin/env ruby
214
+ ++
215
+ ++## git-wt-add: A darcs-style interactive staging script for git. As the
216
+ ++## name implies, git-wt-add walks you through unstaged changes on a
217
+ ++## hunk-by-hunk basis and allows you to pick the ones you'd like staged.
218
+ ++##
219
+ ++## git-wt-add Copyright 2007 William Morgan <wmorgan-git-wt-add@masanjin.net>.
220
+ ++## This program is free software: you can redistribute it and/or modify
221
+ ++## it under the terms of the GNU General Public License as published by
222
+ ++## the Free Software Foundation, either version 3 of the License, or
223
+ ++## (at your option) any later version.
224
+ ++##
225
+ ++## This program is distributed in the hope that it will be useful,
226
+ ++## but WITHOUT ANY WARRANTY; without even the implied warranty of
227
+ ++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
228
+ ++## GNU General Public License for more details.
229
+ ++##
230
+ ++## You can find the GNU General Public License at:
231
+ ++## http://www.gnu.org/licenses/
232
+ ++
233
+ ++COLOR = /\e\[\d*m/
234
+ ++
235
+ ++class Hunk
236
+ ++ attr_reader :file, :file_header, :diff
237
+ ++ attr_accessor :disposition
238
+ ++
239
+ ++ def initialize file, file_header, diff
240
+ ++ @file = file
241
+ ++ @file_header = file_header
242
+ ++ @diff = diff
243
+ ++ @disposition = :unknown
244
+ ++ end
245
+ ++
246
+ ++ def self.make_from diff
247
+ ++ ret = []
248
+ ++ state = :outside
249
+ ++ file_header = hunk = file = nil
250
+ ++
251
+ ++ diff.each do |l| # a little state machine to parse git diff output
252
+ ++ reprocess = false
253
+ ++ begin
254
+ ++ reprocess = false
255
+ ++ case
256
+ ++ when state == :outside && l =~ /^(#{COLOR})*diff --git a\/(.+) b\/(\2)/
257
+ ++ file = $2
258
+ ++ file_header = ""
259
+ ++ when state == :outside && l =~ /^(#{COLOR})*index /
260
+ ++ when state == :outside && l =~ /^(#{COLOR})*(---|\+\+\+) /
261
+ ++ file_header += l + "\n"
262
+ ++ when state == :outside && l =~ /^(#{COLOR})*@@ /
263
+ ++ state = :in_hunk
264
+ ++ hunk = l + "\n"
265
+ ++ when state == :in_hunk && l =~ /^(#{COLOR})*(@@ |diff --git a)/
266
+ ++ ret << Hunk.new(file, file_header, hunk)
267
+ ++ state = :outside
268
+ ++ reprocess = true
269
+ ++ when state == :in_hunk
270
+ ++ hunk += l + "\n"
271
+ ++ else
272
+ ++ raise "unparsable diff input: #{l.inspect}"
273
+ ++ end
274
+ ++ end while reprocess
275
+ ++ end
276
+ ++
277
+ ++ ## add the final hunk
278
+ ++ ret << Hunk.new(file, file_header, hunk) if hunk
279
+ ++
280
+ ++ ret
281
+ ++ end
282
+ ++end
283
+ ++
284
+ ++def help
285
+ ++ puts <<EOS
286
+ ++y: record this patch
287
+ ++n: don't record it
288
+ ++w: wait and decide later, defaulting to no
289
+ ++
290
+ ++s: don't record the rest of the changes to this file
291
+ ++f: record the rest of the changes to this file
292
+ ++
293
+ ++d: record selected patches, skipping all the remaining patches
294
+ ++a: record all the remaining patches
295
+ ++q: cancel record
296
+ ++
297
+ ++j: skip to next patch
298
+ ++k: back up to previous patch
299
+ ++c: calculate number of patches
300
+ ++h or ?: show this help
301
+ ++
302
+ ++<Space>: accept the current default (which is capitalized)
303
+ ++EOS
304
+ ++end
305
+ ++
306
+ ++def walk_through hunks
307
+ ++ skip_files, record_files = {}, {}
308
+ ++ skip_rest = record_rest = false
309
+ ++
310
+ ++ while hunks.any? { |h| h.disposition == :unknown }
311
+ ++ pos = 0
312
+ ++ until pos >= hunks.length
313
+ ++ h = hunks[pos]
314
+ ++ if h.disposition != :unknown
315
+ ++ pos += 1
316
+ ++ next
317
+ ++ elsif skip_rest || skip_files[h.file]
318
+ ++ h.disposition = :ignore
319
+ ++ pos += 1
320
+ ++ next
321
+ ++ elsif record_rest || record_files[h.file]
322
+ ++ h.disposition = :record
323
+ ++ pos += 1
324
+ ++ next
325
+ ++ end
326
+ ++
327
+ ++ puts "Hunk from #{h.file}"
328
+ ++ puts h.diff
329
+ ++ print "Shall I stage this change? (#{pos + 1}/#{hunks.length}) [ynWsfqadk], or ? for help: "
330
+ ++ c = $stdin.getc
331
+ ++ puts
332
+ ++ case c
333
+ ++ when ?y: h.disposition = :record
334
+ ++ when ?n: h.disposition = :ignore
335
+ ++ when ?w, ?\ : h.disposition = :unknown
336
+ ++ when ?s
337
+ ++ h.disposition = :ignore
338
+ ++ skip_files[h.file] = true
339
+ ++ when ?f
340
+ ++ h.disposition = :record
341
+ ++ record_files[h.file] = true
342
+ ++ when ?d: skip_rest = true
343
+ ++ when ?a: record_rest = true
344
+ ++ when ?q: exit
345
+ ++ when ?k
346
+ ++ if pos > 0
347
+ ++ hunks[pos - 1].disposition = :unknown
348
+ ++ pos -= 2 # double-bah
349
+ ++ end
350
+ ++ else
351
+ ++ help
352
+ ++ pos -= 1 # bah
353
+ ++ end
354
+ ++
355
+ ++ pos += 1
356
+ ++ puts
357
+ ++ end
358
+ ++ end
359
+ ++end
360
+ ++
361
+ ++def make_patch hunks
362
+ ++ patch = ""
363
+ ++ did_header = {}
364
+ ++ hunks.each do |h|
365
+ ++ next unless h.disposition == :record
366
+ ++ unless did_header[h.file]
367
+ ++ patch += h.file_header
368
+ ++ did_header[h.file] = true
369
+ ++ end
370
+ ++ patch += h.diff
371
+ ++ end
372
+ ++
373
+ ++ patch.gsub COLOR, ""
374
+ ++end
375
+ ++
376
+ ++### execution starts here ###
377
+ ++
378
+ ++diff = `git diff`.split(/\r?\n/)
379
+ ++if diff.empty?
380
+ ++ puts "No unstaged changes."
381
+ ++ exit
382
+ ++end
383
+ ++hunks = Hunk.make_from diff
384
+ ++
385
+ ++## unix-centric!
386
+ ++state = `stty -g`
387
+ ++begin
388
+ ++ `stty -icanon` # immediate keypress mode
389
+ ++ walk_through hunks
390
+ ++ensure
391
+ ++ `stty #{state}`
392
+ ++end
393
+ ++
394
+ ++patch = make_patch hunks
395
+ ++if patch.empty?
396
+ ++ puts "No changes selected for staging."
397
+ ++else
398
+ ++ IO.popen("git apply --cached", "w") { |f| f.puts patch }
399
+ ++ puts <<EOS
400
+ ++Staged patch of #{patch.split("\n").size} lines.
401
+ ++
402
+ ++Possible next commands:
403
+ ++ git diff --cached: see staged changes
404
+ ++ git commit: commit staged changes
405
+ ++ git reset: unstage changes
406
+ ++EOS
407
+ ++end
408
+ ++
409
+ diff --cc git-wtf
410
+ index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..ea4b27f6812cf9daf84b047bff4443cc554f92bf
411
+ new file mode 100755
412
+ --- /dev/null
413
+ +++ b/git-wtf
414
+ @@@ -1,0 -1,0 +1,223 @@@
415
+ ++#!/usr/bin/env ruby
416
+ ++
417
+ ++## git-wtf: display the state of your repository in a readable and easy-to-scan
418
+ ++## format.
419
+ ++##
420
+ ++## git-wtf tries to ease the task of having many git branches. It's also useful
421
+ ++## for getting a summary of how tracking branches relate to a remote server.
422
+ ++##
423
+ ++## git-wtf shows you:
424
+ ++## - How your branch relates to the remote repo, if it's a tracking branch.
425
+ ++## - How your branch relates to non-feature ("version") branches, if it's a
426
+ ++## feature branch.
427
+ ++## - How your branch relates to the feature branches, if it's a version branch.
428
+ ++##
429
+ ++## For each of these relationships, git-wtf displays the commits pending on
430
+ ++## either side, if any. It displays checkboxes along the side for easy scanning
431
+ ++## of merged/non-merged branches.
432
+ ++##
433
+ ++## If you're working against a remote repo, git-wtf is best used between a 'git
434
+ ++## fetch' and a 'git merge' (or 'git pull' if you don't mind the redundant
435
+ ++## network access).
436
+ ++##
437
+ ++## Usage: git wtf [branch+] [-l|--long] [-a|--all] [--dump-config]
438
+ ++##
439
+ ++## If [branch] is not specified, git-wtf will use the current branch. With
440
+ ++## --long, you'll see author info and date for each commit. With --all, you'll
441
+ ++## see all commits, not just the first 5. With --dump-config, git-wtf will
442
+ ++## print out its current configuration in YAML format and exit.
443
+ ++##
444
+ ++## git-wtf uses some heuristics to determine which branches are version
445
+ ++## branches, and which are feature branches. (Specifically, it assumes the
446
+ ++## version branches are named "master", "next" and "edge".) If it guesses
447
+ ++## incorrectly, you will have to create a .git-wtfrc file.
448
+ ++##
449
+ ++## git-wtf looks for a .git-wtfrc file starting in the current directory, and
450
+ ++## recursively up to the root. The config file is a YAML file that specifies
451
+ ++## the version branches, any branches to ignore, and the max number of commits
452
+ ++## to display when --all isn't used. To start building a configuration file,
453
+ ++## run "git-wtf --dump-config > .git-wtfrc" and edit it.
454
+ ++##
455
+ ++## IMPORTANT NOTE: all local branches referenced in .git-wtfrc must be prefixed
456
+ ++## with heads/, e.g. "heads/master". Remote branches must be of the form
457
+ ++## remotes/<remote>/<branch>.
458
+ ++##
459
+ ++## git-wtf Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
460
+ ++## This program is free software: you can redistribute it and/or modify it
461
+ ++## under the terms of the GNU General Public License as published by the Free
462
+ ++## Software Foundation, either version 3 of the License, or (at your option)
463
+ ++## any later version.
464
+ ++##
465
+ ++## This program is distributed in the hope that it will be useful, but WITHOUT
466
+ ++## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
467
+ ++## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
468
+ ++## more details.
469
+ ++##
470
+ ++## You can find the GNU General Public License at: http://www.gnu.org/licenses/
471
+ ++
472
+ ++
473
+ ++require 'yaml'
474
+ ++CONFIG_FN = ".git-wtfrc"
475
+ ++
476
+ ++class Numeric; def pluralize s; "#{to_s} #{s}" + (self != 1 ? "s" : "") end end
477
+ ++
478
+ ++$long = ARGV.delete("--long") || ARGV.delete("-l")
479
+ ++$all = ARGV.delete("--all") || ARGV.delete("-a")
480
+ ++$dump_config = ARGV.delete("--dump-config")
481
+ ++
482
+ ++## find config file
483
+ ++$config = { "versions" => %w(heads/master heads/next heads/edge), "ignore" => [], "max_commits" => 5 }.merge begin
484
+ ++ p = File.expand_path "."
485
+ ++ fn = while true
486
+ ++ fn = File.join p, CONFIG_FN
487
+ ++ break fn if File.exist? fn
488
+ ++ pp = File.expand_path File.join(p, "..")
489
+ ++ break if p == pp
490
+ ++ p = pp
491
+ ++ end
492
+ ++
493
+ ++ (fn && YAML::load_file(fn)) || {} # YAML turns empty files into false
494
+ ++end
495
+ ++
496
+ ++if $dump_config
497
+ ++ puts $config.to_yaml
498
+ ++ exit(0)
499
+ ++end
500
+ ++
501
+ ++## the set of commits in 'to' that aren't in 'from'.
502
+ ++## if empty, 'to' has been merged into 'from'.
503
+ ++def commits_between from, to
504
+ ++ if $long
505
+ ++ `git log --pretty=format:"- %s [%h] (%ae; %ar)" #{from}..#{to}`
506
+ ++ else
507
+ ++ `git log --pretty=format:"- %s [%h]" #{from}..#{to}`
508
+ ++ end.split(/[\r\n]+/)
509
+ ++end
510
+ ++
511
+ ++def show_commits commits, prefix=" "
512
+ ++ if commits.empty?
513
+ ++ puts "#{prefix} none"
514
+ ++ else
515
+ ++ max = $all ? commits.size : $config["max_commits"]
516
+ ++ max -= 1 if max == commits.size - 1 # never show "and 1 more"
517
+ ++ commits[0 ... max].each { |c| puts "#{prefix}#{c}" }
518
+ ++ puts "#{prefix}... and #{commits.size - max} more." if commits.size > max
519
+ ++ end
520
+ ++end
521
+ ++
522
+ ++def ahead_behind_string ahead, behind
523
+ ++ [ahead.empty? ? nil : "#{ahead.size.pluralize 'commit'} ahead",
524
+ ++ behind.empty? ? nil : "#{behind.size.pluralize 'commit'} behind"].
525
+ ++ compact.join("; ")
526
+ ++end
527
+ ++
528
+ ++def show b, all_branches
529
+ ++ puts "Local branch: #{b[:local_branch]}"
530
+ ++ both = false
531
+ ++
532
+ ++ if b[:remote_branch]
533
+ ++ pushc = commits_between b[:remote_branch], b[:local_branch]
534
+ ++ pullc = commits_between b[:local_branch], b[:remote_branch]
535
+ ++
536
+ ++ both = !pushc.empty? && !pullc.empty?
537
+ ++ if pushc.empty?
538
+ ++ puts "[x] in sync with remote"
539
+ ++ else
540
+ ++ action = both ? "push after rebase / merge" : "push"
541
+ ++ puts "[ ] NOT in sync with remote (needs #{action})"
542
+ ++ show_commits pushc
543
+ ++ end
544
+ ++
545
+ ++ puts "\nRemote branch: #{b[:remote_branch]} (#{b[:remote_url]})"
546
+ ++
547
+ ++ if pullc.empty?
548
+ ++ puts "[x] in sync with local"
549
+ ++ else
550
+ ++ action = pushc.empty? ? "merge" : "rebase / merge"
551
+ ++ puts "[ ] NOT in sync with local (needs #{action})"
552
+ ++ show_commits pullc
553
+ ++
554
+ ++ both = !pushc.empty? && !pullc.empty?
555
+ ++ end
556
+ ++ end
557
+ ++
558
+ ++ vbs, fbs = all_branches.partition { |name, br| $config["versions"].include? br[:local_branch] }
559
+ ++ if $config["versions"].include? b[:local_branch]
560
+ ++ puts "\nFeature branches:" unless fbs.empty?
561
+ ++ fbs.each do |name, br|
562
+ ++ remote_ahead = b[:remote_branch] ? commits_between(b[:remote_branch], br[:local_branch]) : []
563
+ ++ local_ahead = commits_between b[:local_branch], br[:local_branch]
564
+ ++ if local_ahead.empty? && remote_ahead.empty?
565
+ ++ puts "[x] #{br[:name]} is merged in"
566
+ ++ elsif local_ahead.empty? && b[:remote_branch]
567
+ ++ puts "(x) #{br[:name]} merged in (only locally)"
568
+ ++ else
569
+ ++ behind = commits_between br[:local_branch], b[:local_branch]
570
+ ++ puts "[ ] #{br[:name]} is NOT merged in (#{ahead_behind_string local_ahead, behind})"
571
+ ++ show_commits local_ahead
572
+ ++ end
573
+ ++ end
574
+ ++ else
575
+ ++ puts "\nVersion branches:" unless vbs.empty? # unlikely
576
+ ++ vbs.each do |v, br|
577
+ ++ ahead = commits_between v, b[:local_branch]
578
+ ++ if ahead.empty?
579
+ ++ puts "[x] merged into #{v}"
580
+ ++ else
581
+ ++ #behind = commits_between b[:local_branch], v
582
+ ++ puts "[ ] NOT merged into #{v} (#{ahead.size.pluralize 'commit'} ahead)"
583
+ ++ show_commits ahead
584
+ ++ end
585
+ ++ end
586
+ ++ end
587
+ ++
588
+ ++ puts "\nWARNING: local and remote branches have diverged. A merge will occur unless you rebase." if both
589
+ ++end
590
+ ++
591
+ ++branches = `git show-ref`.inject({}) do |hash, l|
592
+ ++ sha1, ref = l.chomp.split " refs/"
593
+ ++ next hash if $config["ignore"].member? ref
594
+ ++ next hash unless ref =~ /^heads\/(.+)/
595
+ ++ name = $1
596
+ ++ hash[name] = { :name => name, :local_branch => ref }
597
+ ++ hash
598
+ ++end
599
+ ++
600
+ ++remotes = `git config --get-regexp ^remote\.\*\.url`.inject({}) do |hash, l|
601
+ ++ l =~ /^remote\.(.+?)\.url (.+)$/ or next hash
602
+ ++ hash[$1] ||= $2
603
+ ++ hash
604
+ ++end
605
+ ++
606
+ ++`git config --get-regexp ^branch\.`.each do |l|
607
+ ++ case l
608
+ ++ when /branch\.(.*?)\.remote (.+)/
609
+ ++ branches[$1] ||= {}
610
+ ++ branches[$1][:remote] = $2
611
+ ++ branches[$1][:remote_url] = remotes[$2]
612
+ ++ when /branch\.(.*?)\.merge ((refs\/)?heads\/)?(.+)/
613
+ ++ branches[$1] ||= {}
614
+ ++ branches[$1][:remote_mergepoint] = $4
615
+ ++ end
616
+ ++end
617
+ ++
618
+ ++branches.each { |k, v| v[:remote_branch] = "#{v[:remote]}/#{v[:remote_mergepoint]}" if v[:remote] && v[:remote_mergepoint] }
619
+ ++
620
+ ++show_dirty = ARGV.empty?
621
+ ++targets = if ARGV.empty?
622
+ ++ [`git symbolic-ref HEAD`.chomp.sub(/^refs\/heads\//, "")]
623
+ ++else
624
+ ++ ARGV
625
+ ++end.map { |t| branches[t] or abort "Error: can't find branch #{t.inspect}." }
626
+ ++
627
+ ++targets.each { |t| show t, branches }
628
+ ++
629
+ ++modified = show_dirty && `git ls-files -m` != ""
630
+ ++uncommitted = show_dirty && `git diff-index --cached HEAD` != ""
631
+ ++
632
+ ++puts if modified || uncommitted
633
+ ++puts "NOTE: working directory contains modified files" if modified
634
+ ++puts "NOTE: staging area contains staged but uncommitted files" if uncommitted
635
+ ++
636
+ ++# the end!
637
+ ++