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