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