Fixes for auto-generation of man pages
[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
37 1. Use "git reset" or similar to undo the effect of the git
38 command(s).
39
40 2. Use "stg repair". This will fix up the StGit metadata to
41 accomodate the modifications to the branch. Specifically, it will
42 do the following:
43
44 * If you have made regular git commits on top of your stack of
45 StGit patches, "stg repair" makes new StGit patches out of
46 them, preserving their contents.
47
48 * However, merge commits cannot become patches; if you have
49 committed a merge on top of your stack, "repair" will simply
50 mark all patches below the merge unapplied, since they are no
51 longer reachable. If this is not what you want, use "git
52 reset" to get rid of the merge and run "stg repair" again.
53
54 * The applied patches are supposed to be precisely those that
55 are reachable from the branch head. If you have used e.g.
56 "git reset" to move the head, some applied patches may no
57 longer be reachable, and some unapplied patches may have
58 become reachable. "stg repair" will correct the appliedness
59 of such patches.
60
61 "stg repair" will fix these inconsistencies reliably, so as long
62 as you like what it does, you have no reason to avoid causing
63 them in the first place. For example, you might find it
64 convenient to make commits with a graphical tool and then have
65 "stg repair" make proper patches of the commits.
66
67NOTE: If using git commands on the stack was a mistake, running "stg
68repair" is _not_ what you want. In that case, what you want is option
69(1) above."""
4d0ba818
KH
70
71options = []
72
575bbdae
KH
73directory = DirectoryGotoToplevel()
74
ca216016
KH
75class Commit(object):
76 def __init__(self, id):
77 self.id = id
78 self.parents = set()
79 self.children = set()
80 self.patch = None
81 self.__commit = None
82 def __get_commit(self):
83 if not self.__commit:
84 self.__commit = git.get_commit(self.id)
85 return self.__commit
86 commit = property(__get_commit)
87 def __str__(self):
88 if self.patch:
89 return '%s (%s)' % (self.id, self.patch)
90 else:
91 return self.id
92 def __repr__(self):
93 return '<%s>' % str(self)
94
95def read_commit_dag(branch):
96 out.start('Reading commit DAG')
97 commits = {}
98 patches = set()
1576d681 99 for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
ca216016
KH
100 cs = line.split()
101 for id in cs:
102 if not id in commits:
103 commits[id] = Commit(id)
104 for id in cs[1:]:
105 commits[cs[0]].parents.add(commits[id])
106 commits[id].children.add(commits[cs[0]])
1576d681 107 for line in Run('git', 'show-ref').output_lines():
ca216016
KH
108 id, ref = line.split()
109 m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
2b049e12 110 if m and not m.group(1).endswith('.log'):
ca216016
KH
111 c = commits[id]
112 c.patch = m.group(1)
113 patches.add(c)
114 out.done()
115 return commits, patches
116
4d0ba818 117def func(parser, options, args):
051090dd 118 """Repair inconsistencies in StGit metadata."""
4d0ba818 119
ca216016
KH
120 orig_applied = crt_series.get_applied()
121 orig_unapplied = crt_series.get_unapplied()
4d0ba818 122
4d0ba818
KH
123 if crt_series.get_protected():
124 raise CmdException(
ca216016
KH
125 'This branch is protected. Modification is not permitted.')
126
051090dd 127 # Find commits that aren't patches, and applied patches.
490add07 128 head = git.get_commit(git.get_head()).get_id_hash()
ca216016
KH
129 commits, patches = read_commit_dag(crt_series.get_name())
130 c = commits[head]
490add07
KH
131 patchify = [] # commits to definitely patchify
132 maybe_patchify = [] # commits to patchify if we find a patch below them
ca216016
KH
133 applied = []
134 while len(c.parents) == 1:
135 parent, = c.parents
136 if c.patch:
137 applied.append(c)
490add07
KH
138 patchify.extend(maybe_patchify)
139 maybe_patchify = []
140 else:
141 maybe_patchify.append(c)
ca216016
KH
142 c = parent
143 applied.reverse()
144 patchify.reverse()
145
146 # Find patches hidden behind a merge.
147 merge = c
148 todo = set([c])
149 seen = set()
150 hidden = set()
151 while todo:
152 c = todo.pop()
153 seen.add(c)
154 todo |= c.parents - seen
155 if c.patch:
156 hidden.add(c)
157 if hidden:
158 out.warn(('%d patch%s are hidden below the merge commit'
159 % (len(hidden), ['es', ''][len(hidden) == 1])),
160 '%s,' % merge.id, 'and will be considered unapplied.')
161
051090dd 162 # Make patches of any linear sequence of commits on top of a patch.
ca216016 163 names = set(p.patch for p in patches)
4d0ba818 164 def name_taken(name):
ca216016
KH
165 return name in names
166 if applied and patchify:
167 out.start('Creating %d new patch%s'
168 % (len(patchify), ['es', ''][len(patchify) == 1]))
169 for p in patchify:
170 name = make_patch_name(p.commit.get_log(), name_taken)
171 out.info('Creating patch %s from commit %s' % (name, p.id))
172 aname, amail, adate = name_email_date(p.commit.get_author())
173 cname, cmail, cdate = name_email_date(p.commit.get_committer())
174 parent, = p.parents
175 crt_series.new_patch(
176 name, can_edit = False, commit = False,
177 top = p.id, bottom = parent.id, message = p.commit.get_log(),
178 author_name = aname, author_email = amail, author_date = adate,
179 committer_name = cname, committer_email = cmail)
180 p.patch = name
181 applied.append(p)
182 names.add(name)
183 out.done()
184
185 # Write the applied/unapplied files.
186 out.start('Checking patch appliedness')
2b049e12 187 unapplied = patches - set(applied)
ca216016 188 applied_name_set = set(p.patch for p in applied)
2b049e12
KH
189 unapplied_name_set = set(p.patch for p in unapplied)
190 patches_name_set = set(p.patch for p in patches)
191 orig_patches = orig_applied + orig_unapplied
192 orig_applied_name_set = set(orig_applied)
193 orig_unapplied_name_set = set(orig_unapplied)
194 orig_patches_name_set = set(orig_patches)
195 for name in orig_patches_name_set - patches_name_set:
196 out.info('%s is gone' % name)
197 for name in applied_name_set - orig_applied_name_set:
198 out.info('%s is now applied' % name)
199 for name in unapplied_name_set - orig_unapplied_name_set:
200 out.info('%s is now unapplied' % name)
201 orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
202 def patchname_cmp(p1, p2):
203 i1 = orig_order.get(p1, len(orig_order))
204 i2 = orig_order.get(p2, len(orig_order))
205 return cmp((i1, p1), (i2, p2))
ca216016 206 crt_series.set_applied(p.patch for p in applied)
2b049e12 207 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
ca216016 208 out.done()