the file is in $2 now.
[git-walkthrough-commit.git] / bin / git-wt-add
blob3125ab5495e5f2ab55ed48af5303031fae02c2ac
1 #!/usr/bin/env ruby
3 ## git-wt-add: A darcs-style interactive staging script for git. As the
4 ## name implies, git-wt-add walks you through unstaged changes on a
5 ## hunk-by-hunk basis and allows you to pick the ones you'd like staged.
6 ##
7 ## git-wt-add Copyright 2007 William Morgan <wmorgan-git-wt-add@masanjin.net>.
8 ## This program is free software: you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published by
10 ## the Free Software Foundation, either version 3 of the License, or
11 ## (at your option) any later version.
13 ## This program is distributed in the hope that it will be useful,
14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ## GNU General Public License for more details.
18 ## You can find the GNU General Public License at:
19 ## http://www.gnu.org/licenses/
21 COLOR = /\e\[\d*m/
23 class Hunk
24 attr_reader :file, :file_header, :diff
25 attr_accessor :disposition
27 def initialize file, file_header, diff
28 @file = file
29 @file_header = file_header
30 @diff = diff
31 @disposition = :unknown
32 end
34 def self.make_from diff
35 ret = []
36 state = :outside
37 file_header = hunk = file = nil
39 diff.each do |l| # a little state machine to parse git diff output
40 reprocess = false
41 begin
42 reprocess = false
43 case
44 when state == :outside && l =~ /^(#{COLOR})*diff --git a\/(.+) b\/(\2)/
45 file = $2
46 file_header = ""
47 when state == :outside && l =~ /^(#{COLOR})*index /
48 when state == :outside && l =~ /^(#{COLOR})*(---|\+\+\+) /
49 file_header += l + "\n"
50 when state == :outside && l =~ /^(#{COLOR})*@@ /
51 state = :in_hunk
52 hunk = l + "\n"
53 when state == :in_hunk && l =~ /^(#{COLOR})*(@@ |diff --git a)/
54 ret << Hunk.new(file, file_header, hunk)
55 state = :outside
56 reprocess = true
57 when state == :in_hunk
58 hunk += l + "\n"
59 else
60 raise "unparsable diff input: #{l.inspect}"
61 end
62 end while reprocess
63 end
65 ## add the final hunk
66 ret << Hunk.new(file, file_header, hunk) if hunk
68 ret
69 end
70 end
72 def help
73 puts <<EOS
74 y: record this patch
75 n: don't record it
76 w: wait and decide later, defaulting to no
78 s: don't record the rest of the changes to this file
79 f: record the rest of the changes to this file
81 d: record selected patches, skipping all the remaining patches
82 a: record all the remaining patches
83 q: cancel record
85 j: skip to next patch
86 k: back up to previous patch
87 c: calculate number of patches
88 h or ?: show this help
90 <Space>: accept the current default (which is capitalized)
91 EOS
92 end
94 def walk_through hunks
95 skip_files, record_files = {}, {}
96 skip_rest = record_rest = false
98 while hunks.any? { |h| h.disposition == :unknown }
99 pos = 0
100 until pos >= hunks.length
101 h = hunks[pos]
102 if h.disposition != :unknown
103 pos += 1
104 next
105 elsif skip_rest || skip_files[h.file]
106 h.disposition = :ignore
107 pos += 1
108 next
109 elsif record_rest || record_files[h.file]
110 h.disposition = :record
111 pos += 1
112 next
115 puts "Hunk from #{h.file}"
116 puts h.diff
117 print "Shall I stage this change? (#{pos + 1}/#{hunks.length}) [ynWsfqadk], or ? for help: "
118 c = $stdin.getc
119 puts
120 case c
121 when ?y: h.disposition = :record
122 when ?n: h.disposition = :ignore
123 when ?w, ?\ : h.disposition = :unknown
124 when ?s
125 h.disposition = :ignore
126 skip_files[h.file] = true
127 when ?f
128 h.disposition = :record
129 record_files[h.file] = true
130 when ?d: skip_rest = true
131 when ?a: record_rest = true
132 when ?q: exit
133 when ?k
134 if pos > 0
135 hunks[pos - 1].disposition = :unknown
136 pos -= 2 # double-bah
138 else
139 help
140 pos -= 1 # bah
143 pos += 1
144 puts
149 def make_patch hunks
150 patch = ""
151 did_header = {}
152 hunks.each do |h|
153 next unless h.disposition == :record
154 unless did_header[h.file]
155 patch += h.file_header
156 did_header[h.file] = true
158 patch += h.diff
161 patch.gsub COLOR, ""
164 ### execution starts here ###
166 diff = `git diff`.split(/\r?\n/)
167 if diff.empty?
168 puts "No unstaged changes."
169 exit
171 hunks = Hunk.make_from diff
173 ## unix-centric!
174 state = `stty -g`
175 begin
176 `stty -icanon` # immediate keypress mode
177 walk_through hunks
178 ensure
179 `stty #{state}`
182 patch = make_patch hunks
183 if patch.empty?
184 puts "No changes selected for staging."
185 else
186 IO.popen("git apply --cached", "w") { |f| f.puts patch }
187 puts <<EOS
188 Staged patch of #{patch.split("\n").size} lines.
190 Possible next commands:
191 git diff --cached: see staged changes
192 git commit: commit staged changes
193 git reset: unstage changes