Teach "stg assimilate" to repair patch reachability
[stgit] / stgit / commands / assimilate.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 "assimilate" 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, "assimilate" 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, "assimilate" 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 "assimilate" 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 "assimilate" will correct the appliedness of such patches.
50
51 Note that these are "inconsistencies", not "errors"; furthermore,
52 "assimilate" will repair them reliably. As long as you are satisfied
53 with the way "assimilate" handles them, you have no reason to avoid
54 causing them in the first place if that is convenient for you."""
55
56 options = []
57
58 class Commit(object):
59 def __init__(self, id):
60 self.id = id
61 self.parents = set()
62 self.children = set()
63 self.patch = None
64 self.__commit = None
65 def __get_commit(self):
66 if not self.__commit:
67 self.__commit = git.get_commit(self.id)
68 return self.__commit
69 commit = property(__get_commit)
70 def __str__(self):
71 if self.patch:
72 return '%s (%s)' % (self.id, self.patch)
73 else:
74 return self.id
75 def __repr__(self):
76 return '<%s>' % str(self)
77
78 def read_commit_dag(branch):
79 out.start('Reading commit DAG')
80 commits = {}
81 patches = set()
82 for line in Run('git-rev-list', '--parents', '--all').output_lines():
83 cs = line.split()
84 for id in cs:
85 if not id in commits:
86 commits[id] = Commit(id)
87 for id in cs[1:]:
88 commits[cs[0]].parents.add(commits[id])
89 commits[id].children.add(commits[cs[0]])
90 for line in Run('git-show-ref').output_lines():
91 id, ref = line.split()
92 m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
93 if m:
94 c = commits[id]
95 c.patch = m.group(1)
96 patches.add(c)
97 out.done()
98 return commits, patches
99
100 def func(parser, options, args):
101 """Assimilate a number of patches.
102 """
103
104 def nothing_to_do():
105 out.info('No commits to assimilate')
106
107 orig_applied = crt_series.get_applied()
108 orig_unapplied = crt_series.get_unapplied()
109
110 # If head == top, we're done.
111 head = git.get_commit(git.get_head()).get_id_hash()
112 top = crt_series.get_current_patch()
113 if top and head == top.get_top():
114 return nothing_to_do()
115
116 if crt_series.get_protected():
117 raise CmdException(
118 'This branch is protected. Modification is not permitted.')
119
120 # Find commits to assimilate, and applied patches.
121 commits, patches = read_commit_dag(crt_series.get_name())
122 c = commits[head]
123 patchify = []
124 applied = []
125 while len(c.parents) == 1:
126 parent, = c.parents
127 if c.patch:
128 applied.append(c)
129 elif not applied:
130 patchify.append(c)
131 c = parent
132 applied.reverse()
133 patchify.reverse()
134
135 # Find patches hidden behind a merge.
136 merge = c
137 todo = set([c])
138 seen = set()
139 hidden = set()
140 while todo:
141 c = todo.pop()
142 seen.add(c)
143 todo |= c.parents - seen
144 if c.patch:
145 hidden.add(c)
146 if hidden:
147 out.warn(('%d patch%s are hidden below the merge commit'
148 % (len(hidden), ['es', ''][len(hidden) == 1])),
149 '%s,' % merge.id, 'and will be considered unapplied.')
150
151 # Assimilate any linear sequence of commits on top of a patch.
152 names = set(p.patch for p in patches)
153 def name_taken(name):
154 return name in names
155 if applied and patchify:
156 out.start('Creating %d new patch%s'
157 % (len(patchify), ['es', ''][len(patchify) == 1]))
158 for p in patchify:
159 name = make_patch_name(p.commit.get_log(), name_taken)
160 out.info('Creating patch %s from commit %s' % (name, p.id))
161 aname, amail, adate = name_email_date(p.commit.get_author())
162 cname, cmail, cdate = name_email_date(p.commit.get_committer())
163 parent, = p.parents
164 crt_series.new_patch(
165 name, can_edit = False, commit = False,
166 top = p.id, bottom = parent.id, message = p.commit.get_log(),
167 author_name = aname, author_email = amail, author_date = adate,
168 committer_name = cname, committer_email = cmail)
169 p.patch = name
170 applied.append(p)
171 names.add(name)
172 out.done()
173
174 # Write the applied/unapplied files.
175 out.start('Checking patch appliedness')
176 applied_name_set = set(p.patch for p in applied)
177 unapplied_names = []
178 for name in orig_applied:
179 if not name in applied_name_set:
180 out.info('%s is now unapplied' % name)
181 unapplied_names.append(name)
182 for name in orig_unapplied:
183 if name in applied_name_set:
184 out.info('%s is now applied' % name)
185 else:
186 unapplied_names.append(name)
187 crt_series.set_applied(p.patch for p in applied)
188 crt_series.set_unapplied(unapplied_names)
189 out.done()