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