Implement "stg refresh --edit" again
[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
KH
71
72options = []
73
117ed129 74directory = DirectoryGotoToplevel(log = True)
575bbdae 75
ca216016
KH
76class Commit(object):
77 def __init__(self, id):
78 self.id = id
79 self.parents = set()
80 self.children = set()
81 self.patch = None
82 self.__commit = None
83 def __get_commit(self):
84 if not self.__commit:
85 self.__commit = git.get_commit(self.id)
86 return self.__commit
87 commit = property(__get_commit)
88 def __str__(self):
89 if self.patch:
90 return '%s (%s)' % (self.id, self.patch)
91 else:
92 return self.id
93 def __repr__(self):
94 return '<%s>' % str(self)
95
96def read_commit_dag(branch):
97 out.start('Reading commit DAG')
98 commits = {}
99 patches = set()
1576d681 100 for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
ca216016
KH
101 cs = line.split()
102 for id in cs:
103 if not id in commits:
104 commits[id] = Commit(id)
105 for id in cs[1:]:
106 commits[cs[0]].parents.add(commits[id])
107 commits[id].children.add(commits[cs[0]])
1576d681 108 for line in Run('git', 'show-ref').output_lines():
ca216016
KH
109 id, ref = line.split()
110 m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
2b049e12 111 if m and not m.group(1).endswith('.log'):
ca216016
KH
112 c = commits[id]
113 c.patch = m.group(1)
114 patches.add(c)
115 out.done()
116 return commits, patches
117
4d0ba818 118def func(parser, options, args):
051090dd 119 """Repair inconsistencies in StGit metadata."""
4d0ba818 120
ca216016
KH
121 orig_applied = crt_series.get_applied()
122 orig_unapplied = crt_series.get_unapplied()
4d0ba818 123
4d0ba818
KH
124 if crt_series.get_protected():
125 raise CmdException(
ca216016
KH
126 'This branch is protected. Modification is not permitted.')
127
051090dd 128 # Find commits that aren't patches, and applied patches.
490add07 129 head = git.get_commit(git.get_head()).get_id_hash()
ca216016
KH
130 commits, patches = read_commit_dag(crt_series.get_name())
131 c = commits[head]
490add07
KH
132 patchify = [] # commits to definitely patchify
133 maybe_patchify = [] # commits to patchify if we find a patch below them
ca216016
KH
134 applied = []
135 while len(c.parents) == 1:
136 parent, = c.parents
137 if c.patch:
138 applied.append(c)
490add07
KH
139 patchify.extend(maybe_patchify)
140 maybe_patchify = []
141 else:
142 maybe_patchify.append(c)
ca216016
KH
143 c = parent
144 applied.reverse()
145 patchify.reverse()
146
147 # Find patches hidden behind a merge.
148 merge = c
149 todo = set([c])
150 seen = set()
151 hidden = set()
152 while todo:
153 c = todo.pop()
154 seen.add(c)
155 todo |= c.parents - seen
156 if c.patch:
157 hidden.add(c)
158 if hidden:
159 out.warn(('%d patch%s are hidden below the merge commit'
160 % (len(hidden), ['es', ''][len(hidden) == 1])),
161 '%s,' % merge.id, 'and will be considered unapplied.')
162
051090dd 163 # Make patches of any linear sequence of commits on top of a patch.
ca216016 164 names = set(p.patch for p in patches)
4d0ba818 165 def name_taken(name):
ca216016
KH
166 return name in names
167 if applied and patchify:
168 out.start('Creating %d new patch%s'
169 % (len(patchify), ['es', ''][len(patchify) == 1]))
170 for p in patchify:
171 name = make_patch_name(p.commit.get_log(), name_taken)
172 out.info('Creating patch %s from commit %s' % (name, p.id))
173 aname, amail, adate = name_email_date(p.commit.get_author())
174 cname, cmail, cdate = name_email_date(p.commit.get_committer())
175 parent, = p.parents
176 crt_series.new_patch(
177 name, can_edit = False, commit = False,
178 top = p.id, bottom = parent.id, message = p.commit.get_log(),
179 author_name = aname, author_email = amail, author_date = adate,
180 committer_name = cname, committer_email = cmail)
181 p.patch = name
182 applied.append(p)
183 names.add(name)
184 out.done()
185
186 # Write the applied/unapplied files.
187 out.start('Checking patch appliedness')
2b049e12 188 unapplied = patches - set(applied)
ca216016 189 applied_name_set = set(p.patch for p in applied)
2b049e12
KH
190 unapplied_name_set = set(p.patch for p in unapplied)
191 patches_name_set = set(p.patch for p in patches)
192 orig_patches = orig_applied + orig_unapplied
193 orig_applied_name_set = set(orig_applied)
194 orig_unapplied_name_set = set(orig_unapplied)
195 orig_patches_name_set = set(orig_patches)
196 for name in orig_patches_name_set - patches_name_set:
197 out.info('%s is gone' % name)
198 for name in applied_name_set - orig_applied_name_set:
199 out.info('%s is now applied' % name)
200 for name in unapplied_name_set - orig_unapplied_name_set:
201 out.info('%s is now unapplied' % name)
202 orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
203 def patchname_cmp(p1, p2):
204 i1 = orig_order.get(p1, len(orig_order))
205 i2 = orig_order.get(p2, len(orig_order))
206 return cmp((i1, p1), (i2, p2))
ca216016 207 crt_series.set_applied(p.patch for p in applied)
2b049e12 208 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
ca216016 209 out.done()