chiark / gitweb /
5a5f979f78be37e5cb55e1e8f23250f2fd840168
[stgit] / stgit / commands / refresh.py
1 # -*- coding: utf-8 -*-
2
3 __copyright__ = """
4 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
5 Copyright (C) 2008, Karl Hasselström <kha@treskal.com>
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
21 from stgit.argparse import opt
22 from stgit.commands import common
23 from stgit.lib import git, transaction, edit
24 from stgit.out import out
25 from stgit import argparse, utils
26
27 help = 'Generate a new commit for the current patch'
28 kind = 'patch'
29 usage = ['[options] [<files or dirs>]']
30 description = """
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
44 squash.
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."""
50
51 args = [argparse.dirty_files]
52 options = [
53     opt('-u', '--update', action = 'store_true',
54         short = 'Only update the current patch files'),
55     opt('-i', '--index', action = 'store_true',
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."""),
59     opt('-p', '--patch', args = [argparse.other_applied_patches,
60                                  argparse.unapplied_patches],
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())
66
67 directory = common.DirectoryHasRepositoryLib()
68
69 def 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
82 def 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
95 def 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
115 def 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
130 def absorb_applied(trans, iw, patch_name, temp_name, edit_fun):
131     """Absorb the temp patch (C{temp_name}) into the given patch
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.
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(
153             edit_fun(trans.patches[patch_name].data.set_tree(temp_cd.tree)))
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
165 def absorb_unapplied(trans, iw, patch_name, temp_name, edit_fun):
166     """Absorb the temp patch (C{temp_name}) into the given patch
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.
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(
194             edit_fun(patch_cd.set_tree(new_tree)))
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
198     else:
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
203 def absorb(stack, patch_name, temp_name, edit_fun):
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]
209     if f(trans, iw, patch_name, temp_name, edit_fun):
210         def info_msg(): pass
211     else:
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
219 def 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
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)