chiark / gitweb /
c36db07f2ebcc4dde795095b62f2278a6fd5e0a6
[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 optparse import OptionParser, make_option
22
23 from stgit.commands.common import *
24 from stgit.utils import *
25 from stgit.out import *
26 from stgit.run import *
27 from stgit import stack, git
28
29 help = 'StGit-ify any git commits made on top of your StGit stack'
30 usage = """%prog [options]
31
32 "repair" will repair three kinds of inconsistencies in your StGit
33 stack, all of them caused by using plain git commands on the branch:
34
35   1. If you have made regular git commits on top of your stack of
36      StGit patches, "repair" converts them to StGit patches,
37      preserving their contents.
38
39   2. Merge commits cannot become patches; if you have committed a
40      merge on top of your stack, "repair" will simply mark all
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
43      rid of the merge and run "repair" again.
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.
49      "repair" will correct the appliedness of such patches.
50
51 Note that these are "inconsistencies", not "errors"; furthermore,
52 "repair" will repair them reliably. As long as you are satisfied
53 with the way "repair" handles them, you have no reason to avoid
54 causing them in the first place if that is convenient for you."""
55
56 directory = DirectoryGotoToplevel()
57 options = []
58
59 class 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
79 def read_commit_dag(branch):
80     out.start('Reading commit DAG')
81     commits = {}
82     patches = set()
83     for line in Run('git', 'rev-list', '--parents', '--all').output_lines():
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]])
91     for line in Run('git', 'show-ref').output_lines():
92         id, ref = line.split()
93         m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
94         if m and not m.group(1).endswith('.log'):
95             c = commits[id]
96             c.patch = m.group(1)
97             patches.add(c)
98     out.done()
99     return commits, patches
100
101 def func(parser, options, args):
102     """Repair inconsistencies in StGit metadata."""
103
104     orig_applied = crt_series.get_applied()
105     orig_unapplied = crt_series.get_unapplied()
106
107     if crt_series.get_protected():
108         raise CmdException(
109             'This branch is protected. Modification is not permitted.')
110
111     # Find commits that aren't patches, and applied patches.
112     head = git.get_commit(git.get_head()).get_id_hash()
113     commits, patches = read_commit_dag(crt_series.get_name())
114     c = commits[head]
115     patchify = []       # commits to definitely patchify
116     maybe_patchify = [] # commits to patchify if we find a patch below them
117     applied = []
118     while len(c.parents) == 1:
119         parent, = c.parents
120         if c.patch:
121             applied.append(c)
122             patchify.extend(maybe_patchify)
123             maybe_patchify = []
124         else:
125             maybe_patchify.append(c)
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
146     # Make patches of any linear sequence of commits on top of a patch.
147     names = set(p.patch for p in patches)
148     def name_taken(name):
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')
171     unapplied = patches - set(applied)
172     applied_name_set = set(p.patch for p in applied)
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))
190     crt_series.set_applied(p.patch for p in applied)
191     crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
192     out.done()