chiark / gitweb /
384cfb958e84d9f8c24de9c6257448db130001c7
[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
24 from stgit.out import out
25 from stgit import 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 coalesce.
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 options = [
52     opt('-u', '--update', action = 'store_true',
53         short = 'Only update the current patch files'),
54     opt('-i', '--index', action = 'store_true',
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."""),
58     opt('-p', '--patch',
59         short = 'Refresh (applied) PATCH instead of the top patch')]
60
61 directory = common.DirectoryHasRepositoryLib()
62
63 def 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
76 def 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
89 def 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
109 def 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
124 def 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
156 def 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
186     else:
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
191 def 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
199     else:
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
207 def 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)