Make "stg repair" help text more helpful
[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
21from optparse import OptionParser, make_option
22
23from stgit.commands.common import *
24from stgit.utils import *
5e888f30 25from stgit.out import *
ca216016 26from stgit.run import *
4d0ba818
KH
27from stgit import stack, git
28
8d2b87ac 29help = 'Fix StGit metadata if branch was modified with git commands'
4d0ba818
KH
30usage = """%prog [options]
31
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 70
7b601c9e 71directory = DirectoryGotoToplevel()
4d0ba818
KH
72options = []
73
ca216016
KH
74class 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
94def read_commit_dag(branch):
95 out.start('Reading commit DAG')
96 commits = {}
97 patches = set()
1576d681 98 for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
ca216016
KH
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]])
1576d681 106 for line in Run('git', 'show-ref').output_lines():
ca216016
KH
107 id, ref = line.split()
108 m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
2b049e12 109 if m and not m.group(1).endswith('.log'):
ca216016
KH
110 c = commits[id]
111 c.patch = m.group(1)
112 patches.add(c)
113 out.done()
114 return commits, patches
115
4d0ba818 116def func(parser, options, args):
051090dd 117 """Repair inconsistencies in StGit metadata."""
4d0ba818 118
ca216016
KH
119 orig_applied = crt_series.get_applied()
120 orig_unapplied = crt_series.get_unapplied()
4d0ba818 121
4d0ba818
KH
122 if crt_series.get_protected():
123 raise CmdException(
ca216016
KH
124 'This branch is protected. Modification is not permitted.')
125
051090dd 126 # Find commits that aren't patches, and applied patches.
490add07 127 head = git.get_commit(git.get_head()).get_id_hash()
ca216016
KH
128 commits, patches = read_commit_dag(crt_series.get_name())
129 c = commits[head]
490add07
KH
130 patchify = [] # commits to definitely patchify
131 maybe_patchify = [] # commits to patchify if we find a patch below them
ca216016
KH
132 applied = []
133 while len(c.parents) == 1:
134 parent, = c.parents
135 if c.patch:
136 applied.append(c)
490add07
KH
137 patchify.extend(maybe_patchify)
138 maybe_patchify = []
139 else:
140 maybe_patchify.append(c)
ca216016
KH
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
051090dd 161 # Make patches of any linear sequence of commits on top of a patch.
ca216016 162 names = set(p.patch for p in patches)
4d0ba818 163 def name_taken(name):
ca216016
KH
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')
2b049e12 186 unapplied = patches - set(applied)
ca216016 187 applied_name_set = set(p.patch for p in applied)
2b049e12
KH
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))
ca216016 205 crt_series.set_applied(p.patch for p in applied)
2b049e12 206 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
ca216016 207 out.done()