chiark / gitweb /
37c4bab1d0cbd350deb919f73ca6b9b7f3c89eba
[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/(.+)$' % 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()