chiark / gitweb /
Add import -p option
[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     opt('-a', '--annotate', metavar = 'NOTE',
65         short = 'Annotate the patch log entry')
66     ] + (argparse.message_options(save_template = False) +
67          argparse.sign_options() + argparse.author_options())
68
69 directory = common.DirectoryHasRepositoryLib()
70
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
132 def absorb_applied(trans, iw, patch_name, temp_name, edit_fun):
133     """Absorb the temp patch (C{temp_name}) into the given patch
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.
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(
155             edit_fun(trans.patches[patch_name].data.set_tree(temp_cd.tree)))
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
167 def absorb_unapplied(trans, iw, patch_name, temp_name, edit_fun):
168     """Absorb the temp patch (C{temp_name}) into the given patch
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.
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(
196             edit_fun(patch_cd.set_tree(new_tree)))
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
200     else:
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
205 def absorb(stack, patch_name, temp_name, edit_fun, annotate = None):
206     """Absorb the temp patch into the target patch."""
207     if annotate:
208         log_msg = 'refresh\n\n' + annotate
209     else:
210         log_msg = 'refresh'
211     trans = transaction.StackTransaction(stack, log_msg)
212     iw = stack.repository.default_iw
213     f = { True: absorb_applied, False: absorb_unapplied
214           }[patch_name in trans.applied]
215     if f(trans, iw, patch_name, temp_name, edit_fun):
216         def info_msg(): pass
217     else:
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
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
261     return absorb(stack, patch_name, temp_name, edit_fun,
262                   annotate = options.annotate)