Rename "stg coalesce" to "stg squash"
[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 22from stgit.commands import common
f9d69fc4 23from stgit.lib import git, transaction, edit
85aaed81 24from stgit.out import out
f9d69fc4 25from stgit import argparse, 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
594aa463 44squash.
85aaed81
KH
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
6c8a90e1 51args = [argparse.dirty_files]
575bbdae 52options = [
85aaed81 53 opt('-u', '--update', action = 'store_true',
575bbdae 54 short = 'Only update the current patch files'),
85aaed81 55 opt('-i', '--index', action = 'store_true',
575bbdae
KH
56 short = 'Refresh from index instead of worktree', long = """
57 Instead of setting the patch top to the current contents of
58 the worktree, set it to the current contents of the index."""),
6c8a90e1
KH
59 opt('-p', '--patch', args = [argparse.other_applied_patches,
60 argparse.unapplied_patches],
f9d69fc4
KH
61 short = 'Refresh (applied) PATCH instead of the top patch'),
62 opt('-e', '--edit', action = 'store_true',
63 short = 'Invoke an editor for the patch description'),
64 ] + (argparse.message_options(save_template = False) +
65 argparse.sign_options() + argparse.author_options())
575bbdae 66
85aaed81 67directory = common.DirectoryHasRepositoryLib()
fcee87cf 68
85aaed81
KH
69def get_patch(stack, given_patch):
70 """Get the name of the patch we are to refresh."""
71 if given_patch:
72 patch_name = given_patch
73 if not stack.patches.exists(patch_name):
74 raise common.CmdException('%s: no such patch' % patch_name)
75 return patch_name
76 else:
77 if not stack.patchorder.applied:
78 raise common.CmdException(
79 'Cannot refresh top patch, because no patches are applied')
80 return stack.patchorder.applied[-1]
81
82def list_files(stack, patch_name, args, index, update):
83 """Figure out which files to update."""
84 if index:
85 # --index: Don't update the index.
86 return set()
87 paths = stack.repository.default_iw.changed_files(
88 stack.head.data.tree, args or [])
89 if update:
90 # --update: Restrict update to the paths that were already
91 # part of the patch.
92 paths &= stack.patches.get(patch_name).files()
93 return paths
94
95def write_tree(stack, paths, temp_index):
96 """Possibly update the index, and then write its tree.
97 @return: The written tree.
98 @rtype: L{Tree<stgit.git.Tree>}"""
99 def go(index):
100 if paths:
101 iw = git.IndexAndWorktree(index, stack.repository.default_worktree)
102 iw.update_index(paths)
103 return index.write_tree()
104 if temp_index:
105 index = stack.repository.temp_index()
106 try:
107 index.read_tree(stack.head)
108 return go(index)
109 finally:
110 index.delete()
111 stack.repository.default_iw.update_index(paths)
112 else:
113 return go(stack.repository.default_index)
114
115def make_temp_patch(stack, patch_name, paths, temp_index):
116 """Commit index to temp patch, in a complete transaction. If any path
117 limiting is in effect, use a temp index."""
118 tree = write_tree(stack, paths, temp_index)
119 commit = stack.repository.commit(git.CommitData(
120 tree = tree, parents = [stack.head],
121 message = 'Refresh of %s' % patch_name))
122 temp_name = utils.make_patch_name('refresh-temp', stack.patches.exists)
123 trans = transaction.StackTransaction(stack,
124 'refresh (create temporary patch)')
125 trans.patches[temp_name] = commit
126 trans.applied.append(temp_name)
127 return trans.run(stack.repository.default_iw,
128 print_current_patch = False), temp_name
129
f9d69fc4 130def absorb_applied(trans, iw, patch_name, temp_name, edit_fun):
85aaed81 131 """Absorb the temp patch (C{temp_name}) into the given patch
f9d69fc4
KH
132 (C{patch_name}), which must be applied. If the absorption
133 succeeds, call C{edit_fun} on the resulting
134 L{CommitData<stgit.lib.git.CommitData>} before committing it and
135 commit the return value.
85aaed81
KH
136
137 @return: C{True} if we managed to absorb the temp patch, C{False}
138 if we had to leave it for the user to deal with."""
139 temp_absorbed = False
140 try:
141 # Pop any patch on top of the patch we're refreshing.
142 to_pop = trans.applied[trans.applied.index(patch_name)+1:]
143 if len(to_pop) > 1:
144 popped = trans.pop_patches(lambda pn: pn in to_pop)
145 assert not popped # no other patches were popped
146 trans.push_patch(temp_name, iw)
147 assert to_pop.pop() == temp_name
148
149 # Absorb the temp patch.
150 temp_cd = trans.patches[temp_name].data
151 assert trans.patches[patch_name] == temp_cd.parent
152 trans.patches[patch_name] = trans.stack.repository.commit(
f9d69fc4 153 edit_fun(trans.patches[patch_name].data.set_tree(temp_cd.tree)))
85aaed81
KH
154 popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
155 assert not popped # the temp patch was topmost
156 temp_absorbed = True
157
158 # Push back any patch we were forced to pop earlier.
159 for pn in to_pop:
160 trans.push_patch(pn, iw)
161 except transaction.TransactionHalted:
162 pass
163 return temp_absorbed
164
f9d69fc4 165def absorb_unapplied(trans, iw, patch_name, temp_name, edit_fun):
85aaed81 166 """Absorb the temp patch (C{temp_name}) into the given patch
f9d69fc4
KH
167 (C{patch_name}), which must be unapplied. If the absorption
168 succeeds, call C{edit_fun} on the resulting
169 L{CommitData<stgit.lib.git.CommitData>} before committing it and
170 commit the return value.
85aaed81
KH
171
172 @param iw: Not used.
173 @return: C{True} if we managed to absorb the temp patch, C{False}
174 if we had to leave it for the user to deal with."""
175
176 # Pop the temp patch.
177 popped = trans.pop_patches(lambda pn: pn == temp_name)
178 assert not popped # the temp patch was topmost
179
180 # Try to create the new tree of the refreshed patch. (This is the
181 # same operation as pushing the temp patch onto the patch we're
182 # trying to refresh -- but we don't have a worktree to spill
183 # conflicts to, so if the simple merge doesn't succeed, we have to
184 # give up.)
185 patch_cd = trans.patches[patch_name].data
186 temp_cd = trans.patches[temp_name].data
187 new_tree = trans.stack.repository.simple_merge(
188 base = temp_cd.parent.data.tree,
189 ours = patch_cd.tree, theirs = temp_cd.tree)
190 if new_tree:
191 # It worked. Refresh the patch with the new tree, and delete
192 # the temp patch.
193 trans.patches[patch_name] = trans.stack.repository.commit(
f9d69fc4 194 edit_fun(patch_cd.set_tree(new_tree)))
85aaed81
KH
195 popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True)
196 assert not popped # the temp patch was not applied
197 return True
42fc7623 198 else:
85aaed81
KH
199 # Nope, we couldn't create the new tree, so we'll just have to
200 # leave the temp patch for the user.
201 return False
202
f9d69fc4 203def absorb(stack, patch_name, temp_name, edit_fun):
85aaed81
KH
204 """Absorb the temp patch into the target patch."""
205 trans = transaction.StackTransaction(stack, 'refresh')
206 iw = stack.repository.default_iw
207 f = { True: absorb_applied, False: absorb_unapplied
208 }[patch_name in trans.applied]
f9d69fc4 209 if f(trans, iw, patch_name, temp_name, edit_fun):
85aaed81 210 def info_msg(): pass
fcee87cf 211 else:
85aaed81
KH
212 def info_msg():
213 out.warn('The new changes did not apply cleanly to %s.'
214 % patch_name, 'They were saved in %s.' % temp_name)
215 r = trans.run(iw)
216 info_msg()
217 return r
218
219def func(parser, options, args):
220 """Generate a new commit for the current or given patch."""
221
222 # Catch illegal argument combinations.
223 path_limiting = bool(args or options.update)
224 if options.index and path_limiting:
225 raise common.CmdException(
226 'Only full refresh is available with the --index option')
227
228 stack = directory.repository.current_stack
229 patch_name = get_patch(stack, options.patch)
230 paths = list_files(stack, patch_name, args, options.index, options.update)
231
232 # Make sure there are no conflicts in the files we want to
233 # refresh.
234 if stack.repository.default_index.conflicts() & paths:
235 raise common.CmdException(
236 'Cannot refresh -- resolve conflicts first')
237
238 # Commit index to temp patch, and absorb it into the target patch.
239 retval, temp_name = make_temp_patch(
240 stack, patch_name, paths, temp_index = path_limiting)
241 if retval != utils.STGIT_SUCCESS:
242 return retval
f9d69fc4
KH
243 def edit_fun(cd):
244 cd, failed_diff = edit.auto_edit_patch(
245 stack.repository, cd, msg = options.message, contains_diff = False,
246 author = options.author, committer = lambda p: p,
247 sign_str = options.sign_str)
248 assert not failed_diff
249 if options.edit:
250 cd, failed_diff = edit.interactive_edit_patch(
251 stack.repository, cd, edit_diff = False,
252 diff_flags = [], replacement_diff = None)
253 assert not failed_diff
254 return cd
255 return absorb(stack, patch_name, temp_name, edit_fun)