Fix stg repair for hidden patches
[stgit] / stgit / commands / repair.py
CommitLineData
4d0ba818
KH
1# -*- coding: utf-8 -*-
2
3__copyright__ = """
4Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
5
6This program is free software; you can redistribute it and/or modify
7it under the terms of the GNU General Public License version 2 as
8published by the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18"""
19
20import sys, os
575bbdae 21from stgit.argparse import opt
4d0ba818
KH
22from stgit.commands.common import *
23from stgit.utils import *
5e888f30 24from stgit.out import *
ca216016 25from stgit.run import *
4d0ba818
KH
26from stgit import stack, git
27
8d2b87ac 28help = 'Fix StGit metadata if branch was modified with git commands'
33ff9cdd 29kind = 'stack'
575bbdae
KH
30usage = ['']
31description = """
8d2b87ac
KH
32If you modify an StGit stack (branch) with some git commands -- such
33as commit, pull, merge, and rebase -- you will leave the StGit
34metadata in an inconsistent state. In that situation, you have two
35options:
36
625e8de7
KH
37 1. Use "stg undo" to undo the effect of the git commands. (If you
38 know what you are doing and want more control, "git reset" or
39 similar will work too.)
8d2b87ac
KH
40
41 2. Use "stg repair". This will fix up the StGit metadata to
42 accomodate the modifications to the branch. Specifically, it will
43 do the following:
44
45 * If you have made regular git commits on top of your stack of
46 StGit patches, "stg repair" makes new StGit patches out of
47 them, preserving their contents.
48
49 * However, merge commits cannot become patches; if you have
50 committed a merge on top of your stack, "repair" will simply
51 mark all patches below the merge unapplied, since they are no
625e8de7
KH
52 longer reachable. If this is not what you want, use "stg
53 undo" to get rid of the merge and run "stg repair" again.
8d2b87ac
KH
54
55 * The applied patches are supposed to be precisely those that
56 are reachable from the branch head. If you have used e.g.
57 "git reset" to move the head, some applied patches may no
58 longer be reachable, and some unapplied patches may have
59 become reachable. "stg repair" will correct the appliedness
60 of such patches.
61
62 "stg repair" will fix these inconsistencies reliably, so as long
63 as you like what it does, you have no reason to avoid causing
64 them in the first place. For example, you might find it
65 convenient to make commits with a graphical tool and then have
66 "stg repair" make proper patches of the commits.
67
68NOTE: If using git commands on the stack was a mistake, running "stg
69repair" is _not_ what you want. In that case, what you want is option
70(1) above."""
4d0ba818 71
6c8a90e1 72args = []
4d0ba818
KH
73options = []
74
117ed129 75directory = DirectoryGotoToplevel(log = True)
575bbdae 76
ca216016
KH
77class Commit(object):
78 def __init__(self, id):
79 self.id = id
80 self.parents = set()
81 self.children = set()
82 self.patch = None
83 self.__commit = None
84 def __get_commit(self):
85 if not self.__commit:
86 self.__commit = git.get_commit(self.id)
87 return self.__commit
88 commit = property(__get_commit)
89 def __str__(self):
90 if self.patch:
91 return '%s (%s)' % (self.id, self.patch)
92 else:
93 return self.id
94 def __repr__(self):
95 return '<%s>' % str(self)
96
97def read_commit_dag(branch):
98 out.start('Reading commit DAG')
99 commits = {}
100 patches = set()
1576d681 101 for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
ca216016
KH
102 cs = line.split()
103 for id in cs:
104 if not id in commits:
105 commits[id] = Commit(id)
106 for id in cs[1:]:
107 commits[cs[0]].parents.add(commits[id])
108 commits[id].children.add(commits[cs[0]])
1576d681 109 for line in Run('git', 'show-ref').output_lines():
ca216016 110 id, ref = line.split()
ce4c86fa 111 m = re.match(r'^refs/patches/%s/(.+)$' % re.escape(branch), ref)
2b049e12 112 if m and not m.group(1).endswith('.log'):
ca216016
KH
113 c = commits[id]
114 c.patch = m.group(1)
115 patches.add(c)
116 out.done()
117 return commits, patches
118
4d0ba818 119def func(parser, options, args):
051090dd 120 """Repair inconsistencies in StGit metadata."""
4d0ba818 121
ca216016
KH
122 orig_applied = crt_series.get_applied()
123 orig_unapplied = crt_series.get_unapplied()
2d0f10fb 124 orig_hidden = crt_series.get_hidden()
4d0ba818 125
4d0ba818
KH
126 if crt_series.get_protected():
127 raise CmdException(
ca216016
KH
128 'This branch is protected. Modification is not permitted.')
129
051090dd 130 # Find commits that aren't patches, and applied patches.
490add07 131 head = git.get_commit(git.get_head()).get_id_hash()
ca216016
KH
132 commits, patches = read_commit_dag(crt_series.get_name())
133 c = commits[head]
490add07
KH
134 patchify = [] # commits to definitely patchify
135 maybe_patchify = [] # commits to patchify if we find a patch below them
ca216016
KH
136 applied = []
137 while len(c.parents) == 1:
138 parent, = c.parents
139 if c.patch:
140 applied.append(c)
490add07
KH
141 patchify.extend(maybe_patchify)
142 maybe_patchify = []
143 else:
144 maybe_patchify.append(c)
ca216016
KH
145 c = parent
146 applied.reverse()
147 patchify.reverse()
148
149 # Find patches hidden behind a merge.
150 merge = c
151 todo = set([c])
152 seen = set()
153 hidden = set()
154 while todo:
155 c = todo.pop()
156 seen.add(c)
157 todo |= c.parents - seen
158 if c.patch:
159 hidden.add(c)
160 if hidden:
161 out.warn(('%d patch%s are hidden below the merge commit'
162 % (len(hidden), ['es', ''][len(hidden) == 1])),
163 '%s,' % merge.id, 'and will be considered unapplied.')
164
051090dd 165 # Make patches of any linear sequence of commits on top of a patch.
ca216016 166 names = set(p.patch for p in patches)
4d0ba818 167 def name_taken(name):
ca216016
KH
168 return name in names
169 if applied and patchify:
170 out.start('Creating %d new patch%s'
171 % (len(patchify), ['es', ''][len(patchify) == 1]))
172 for p in patchify:
173 name = make_patch_name(p.commit.get_log(), name_taken)
174 out.info('Creating patch %s from commit %s' % (name, p.id))
175 aname, amail, adate = name_email_date(p.commit.get_author())
176 cname, cmail, cdate = name_email_date(p.commit.get_committer())
177 parent, = p.parents
178 crt_series.new_patch(
179 name, can_edit = False, commit = False,
180 top = p.id, bottom = parent.id, message = p.commit.get_log(),
181 author_name = aname, author_email = amail, author_date = adate,
182 committer_name = cname, committer_email = cmail)
183 p.patch = name
184 applied.append(p)
185 names.add(name)
186 out.done()
187
2d0f10fb
JW
188 # Figure out hidden
189 orig_patches = orig_applied + orig_unapplied + orig_hidden
190 orig_applied_name_set = set(orig_applied)
191 orig_unapplied_name_set = set(orig_unapplied)
192 orig_hidden_name_set = set(orig_hidden)
193 orig_patches_name_set = set(orig_patches)
194 hidden = [p for p in patches if p.patch in orig_hidden_name_set]
195
ca216016
KH
196 # Write the applied/unapplied files.
197 out.start('Checking patch appliedness')
2d0f10fb 198 unapplied = patches - set(applied) - set(hidden)
ca216016 199 applied_name_set = set(p.patch for p in applied)
2b049e12 200 unapplied_name_set = set(p.patch for p in unapplied)
2d0f10fb 201 hidden_name_set = set(p.patch for p in hidden)
2b049e12 202 patches_name_set = set(p.patch for p in patches)
2b049e12
KH
203 for name in orig_patches_name_set - patches_name_set:
204 out.info('%s is gone' % name)
205 for name in applied_name_set - orig_applied_name_set:
206 out.info('%s is now applied' % name)
207 for name in unapplied_name_set - orig_unapplied_name_set:
208 out.info('%s is now unapplied' % name)
2d0f10fb
JW
209 for name in hidden_name_set - orig_hidden_name_set:
210 out.info('%s is now hidden' % name)
2b049e12
KH
211 orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
212 def patchname_cmp(p1, p2):
213 i1 = orig_order.get(p1, len(orig_order))
214 i2 = orig_order.get(p2, len(orig_order))
215 return cmp((i1, p1), (i2, p2))
ca216016 216 crt_series.set_applied(p.patch for p in applied)
2b049e12 217 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
2d0f10fb 218 crt_series.set_hidden(sorted(hidden_name_set, cmp = patchname_cmp))
ca216016 219 out.done()