| 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) |