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