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