Slightly modify the "publish" command description
[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 kind = 'stack'
30 usage = ['']
31 description = """
32 If you modify an StGit stack (branch) with some git commands -- such
33 as commit, pull, merge, and rebase -- you will leave the StGit
34 metadata in an inconsistent state. In that situation, you have two
35 options:
36
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.)
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
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.
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
68 NOTE: If using git commands on the stack was a mistake, running "stg
69 repair" is _not_ what you want. In that case, what you want is option
70 (1) above."""
71
72 args = []
73 options = []
74
75 directory = DirectoryGotoToplevel(log = True)
76
77 class 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
97 def read_commit_dag(branch):
98 out.start('Reading commit DAG')
99 commits = {}
100 patches = set()
101 for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
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]])
109 for line in Run('git', 'show-ref').output_lines():
110 id, ref = line.split()
111 m = re.match(r'^refs/patches/%s/(.+)$' % re.escape(branch), ref)
112 if m and not m.group(1).endswith('.log'):
113 c = commits[id]
114 c.patch = m.group(1)
115 patches.add(c)
116 out.done()
117 return commits, patches
118
119 def func(parser, options, args):
120 """Repair inconsistencies in StGit metadata."""
121
122 orig_applied = crt_series.get_applied()
123 orig_unapplied = crt_series.get_unapplied()
124
125 if crt_series.get_protected():
126 raise CmdException(
127 'This branch is protected. Modification is not permitted.')
128
129 # Find commits that aren't patches, and applied patches.
130 head = git.get_commit(git.get_head()).get_id_hash()
131 commits, patches = read_commit_dag(crt_series.get_name())
132 c = commits[head]
133 patchify = [] # commits to definitely patchify
134 maybe_patchify = [] # commits to patchify if we find a patch below them
135 applied = []
136 while len(c.parents) == 1:
137 parent, = c.parents
138 if c.patch:
139 applied.append(c)
140 patchify.extend(maybe_patchify)
141 maybe_patchify = []
142 else:
143 maybe_patchify.append(c)
144 c = parent
145 applied.reverse()
146 patchify.reverse()
147
148 # Find patches hidden behind a merge.
149 merge = c
150 todo = set([c])
151 seen = set()
152 hidden = set()
153 while todo:
154 c = todo.pop()
155 seen.add(c)
156 todo |= c.parents - seen
157 if c.patch:
158 hidden.add(c)
159 if hidden:
160 out.warn(('%d patch%s are hidden below the merge commit'
161 % (len(hidden), ['es', ''][len(hidden) == 1])),
162 '%s,' % merge.id, 'and will be considered unapplied.')
163
164 # Make patches of any linear sequence of commits on top of a patch.
165 names = set(p.patch for p in patches)
166 def name_taken(name):
167 return name in names
168 if applied and patchify:
169 out.start('Creating %d new patch%s'
170 % (len(patchify), ['es', ''][len(patchify) == 1]))
171 for p in patchify:
172 name = make_patch_name(p.commit.get_log(), name_taken)
173 out.info('Creating patch %s from commit %s' % (name, p.id))
174 aname, amail, adate = name_email_date(p.commit.get_author())
175 cname, cmail, cdate = name_email_date(p.commit.get_committer())
176 parent, = p.parents
177 crt_series.new_patch(
178 name, can_edit = False, commit = False,
179 top = p.id, bottom = parent.id, message = p.commit.get_log(),
180 author_name = aname, author_email = amail, author_date = adate,
181 committer_name = cname, committer_email = cmail)
182 p.patch = name
183 applied.append(p)
184 names.add(name)
185 out.done()
186
187 # Write the applied/unapplied files.
188 out.start('Checking patch appliedness')
189 unapplied = patches - set(applied)
190 applied_name_set = set(p.patch for p in applied)
191 unapplied_name_set = set(p.patch for p in unapplied)
192 patches_name_set = set(p.patch for p in patches)
193 orig_patches = orig_applied + orig_unapplied
194 orig_applied_name_set = set(orig_applied)
195 orig_unapplied_name_set = set(orig_unapplied)
196 orig_patches_name_set = set(orig_patches)
197 for name in orig_patches_name_set - patches_name_set:
198 out.info('%s is gone' % name)
199 for name in applied_name_set - orig_applied_name_set:
200 out.info('%s is now applied' % name)
201 for name in unapplied_name_set - orig_unapplied_name_set:
202 out.info('%s is now unapplied' % name)
203 orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
204 def patchname_cmp(p1, p2):
205 i1 = orig_order.get(p1, len(orig_order))
206 i2 = orig_order.get(p2, len(orig_order))
207 return cmp((i1, p1), (i2, p2))
208 crt_series.set_applied(p.patch for p in applied)
209 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
210 out.done()