Invoke the correct interactive editor
[stgit] / stgit / commands / refresh.py
CommitLineData
85aaed81 1# -*- coding: utf-8 -*-
fcee87cf
CM
2
3__copyright__ = """
4Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
85aaed81 5Copyright (C) 2008, Karl Hasselström <kha@treskal.com>
fcee87cf
CM
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
575bbdae 21from stgit.argparse import opt
85aaed81
KH
22from stgit.commands import common
23from stgit.lib import git, transaction
24from stgit.out import out
25from stgit import utils
fcee87cf 26
575bbdae 27help = 'Generate a new commit for the current patch'
33ff9cdd 28kind = 'patch'
575bbdae
KH
29usage = ['[options] [<files or dirs>]']
30description = """
85aaed81
KH
31Include the latest work tree and index changes in the current patch.
32This command generates a new git commit object for the patch; the old
33commit is no longer visible.
34
35You may optionally list one or more files or directories relative to
36the current working directory; if you do, only matching files will be
37updated.
38
39Behind the scenes, stg refresh first creates a new temporary patch
40with your updates, and then merges that patch into the patch you asked
41to have refreshed. If you asked to refresh a patch other than the
42topmost patch, there can be conflicts; in that case, the temporary
43patch will be left for you to take care of, for example with stg
44coalesce.
45
46The creation of the temporary patch is recorded in a separate entry in
47the patch stack log; this means that one undo step will undo the merge
48between the other patch and the temp patch, and two undo steps will
49additionally get rid of the temp patch."""
fcee87cf 50
575bbdae 51options = [
85aaed81 52 opt('-u', '--update', action = 'store_true',
575bbdae 53 short = 'Only update the current patch files'),
85aaed81 54 opt('-i', '--index', action = 'store_true',
575bbdae
KH
55 short = 'Refresh from index instead of worktree', long = """
56 Instead of setting the patch top to the current contents of
57 the worktree, set it to the current contents of the index."""),
575bbdae
KH
58 opt('-p', '--patch',
59 short = 'Refresh (applied) PATCH instead of the top patch')]
60
85aaed81 61directory = common.DirectoryHasRepositoryLib()
fcee87cf 62
85aaed81
KH
63def get_patch(stack, given_patch):
64 """Get the name of the patch we are to refresh."""
65 if given_patch:
66 patch_name = given_patch
67 if not stack.patches.exists(patch_name):
68 raise common.CmdException('%s: no such patch' % patch_name)
69 return patch_name
70 else:
71 if not stack.patchorder.applied:
72 raise common.CmdException(
73 'Cannot refresh top patch, because no patches are applied')
74 return stack.patchorder.applied[-1]
75
76def list_files(stack, patch_name, args, index, update):
77 """Figure out which files to update."""
78 if index:
79 # --index: Don't update the index.
80 return set()
81 paths = stack.repository.default_iw.changed_files(
82 stack.head.data.tree, args or [])
83 if update:
84 # --update: Restrict update to the paths that were already
85 # part of the patch.
86 paths &= stack.patches.get(patch_name).files()
87 return paths
88
89def write_tree(stack, paths, temp_index):
90 """Possibly update the index, and then write its tree.
91 @return: The written tree.
92 @rtype: L{Tree<stgit.git.Tree>}"""
93 def go(index):
94 if paths:
95 iw = git.IndexAndWorktree(index, stack.repository.default_worktree)
96 iw.update_index(paths)
97 return index.write_tree()
98 if temp_index:
99 index = stack.repository.temp_index()
100 try:
101 index.read_tree(stack.head)
102 return go(index)
103 finally:
104 index.delete()
105 stack.repository.default_iw.update_index(paths)
106 else:
107 return go(stack.repository.default_index)
108
109def make_temp_patch(stack, patch_name, paths, temp_index):
110 """Commit index to temp patch, in a complete transaction. If any path
111 limiting is in effect, use a temp index."""
112 tree = write_tree(stack, paths, temp_index)
113 commit = stack.repository.commit(git.CommitData(
114 tree = tree, parents = [stack.head],
115 message = 'Refresh of %s' % patch_name))
116 temp_name = utils.make_patch_name('refresh-temp', stack.patches.exists)
117 trans = transaction.StackTransaction(stack,
118 'refresh (create temporary patch)')
119 trans.patches[temp_name] = commit
120 trans.applied.append(temp_name)
121 return trans.run(stack.repository.default_iw,
122 print_current_patch = False), temp_name
123
124def absorb_applied(trans, iw, patch_name, temp_name):
125 """Absorb the temp patch (C{temp_name}) into the given patch
126 (C{patch_name}), which must be applied.
127
128 @return: C{True} if we managed to absorb the temp patch, C{False}
129 if we had to leave it for the user to deal with."""
130 temp_absorbed = False
131 try:
132 # Pop any patch on top of the patch we're refreshing.
133 to_pop = trans.applied[trans.applied.index(patch_name)+1:]
134 if len(to_pop) > 1:
135 popped = trans.pop_patches(lambda pn: pn in to_pop)
136 assert not popped # no other patches were popped
137 trans.push_patch(temp_name, iw)
138 assert to_pop.pop() == temp_name
139
140 # Absorb the temp patch.
141 temp_cd = trans.patches[temp_name].data
142 assert trans.patches[patch_name] == temp_cd.parent
143 trans.patches[patch_name] = trans.stack.repository.commit(
144 trans.patches[patch_name].data.set_tree(temp_cd.tree))
145 popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
146 assert not popped # the temp patch was topmost
147 temp_absorbed = True
148
149 # Push back any patch we were forced to pop earlier.
150 for pn in to_pop:
151 trans.push_patch(pn, iw)
152 except transaction.TransactionHalted:
153 pass
154 return temp_absorbed
155
156def absorb_unapplied(trans, iw, patch_name, temp_name):
157 """Absorb the temp patch (C{temp_name}) into the given patch
158 (C{patch_name}), which must be unapplied.
159
160 @param iw: Not used.
161 @return: C{True} if we managed to absorb the temp patch, C{False}
162 if we had to leave it for the user to deal with."""
163
164 # Pop the temp patch.
165 popped = trans.pop_patches(lambda pn: pn == temp_name)
166 assert not popped # the temp patch was topmost
167
168 # Try to create the new tree of the refreshed patch. (This is the
169 # same operation as pushing the temp patch onto the patch we're
170 # trying to refresh -- but we don't have a worktree to spill
171 # conflicts to, so if the simple merge doesn't succeed, we have to
172 # give up.)
173 patch_cd = trans.patches[patch_name].data
174 temp_cd = trans.patches[temp_name].data
175 new_tree = trans.stack.repository.simple_merge(
176 base = temp_cd.parent.data.tree,
177 ours = patch_cd.tree, theirs = temp_cd.tree)
178 if new_tree:
179 # It worked. Refresh the patch with the new tree, and delete
180 # the temp patch.
181 trans.patches[patch_name] = trans.stack.repository.commit(
182 patch_cd.set_tree(new_tree))
183 popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
184 assert not popped # the temp patch was not applied
185 return True
42fc7623 186 else:
85aaed81
KH
187 # Nope, we couldn't create the new tree, so we'll just have to
188 # leave the temp patch for the user.
189 return False
190
191def absorb(stack, patch_name, temp_name):
192 """Absorb the temp patch into the target patch."""
193 trans = transaction.StackTransaction(stack, 'refresh')
194 iw = stack.repository.default_iw
195 f = { True: absorb_applied, False: absorb_unapplied
196 }[patch_name in trans.applied]
197 if f(trans, iw, patch_name, temp_name):
198 def info_msg(): pass
fcee87cf 199 else:
85aaed81
KH
200 def info_msg():
201 out.warn('The new changes did not apply cleanly to %s.'
202 % patch_name, 'They were saved in %s.' % temp_name)
203 r = trans.run(iw)
204 info_msg()
205 return r
206
207def func(parser, options, args):
208 """Generate a new commit for the current or given patch."""
209
210 # Catch illegal argument combinations.
211 path_limiting = bool(args or options.update)
212 if options.index and path_limiting:
213 raise common.CmdException(
214 'Only full refresh is available with the --index option')
215
216 stack = directory.repository.current_stack
217 patch_name = get_patch(stack, options.patch)
218 paths = list_files(stack, patch_name, args, options.index, options.update)
219
220 # Make sure there are no conflicts in the files we want to
221 # refresh.
222 if stack.repository.default_index.conflicts() & paths:
223 raise common.CmdException(
224 'Cannot refresh -- resolve conflicts first')
225
226 # Commit index to temp patch, and absorb it into the target patch.
227 retval, temp_name = make_temp_patch(
228 stack, patch_name, paths, temp_index = path_limiting)
229 if retval != utils.STGIT_SUCCESS:
230 return retval
231 return absorb(stack, patch_name, temp_name)