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