chiark / gitweb /
Infrastructure for current directory handling
[stgit] / stgit / commands / assimilate.py
CommitLineData
4d0ba818
KH
1# -*- coding: utf-8 -*-
2
3__copyright__ = """
4Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
5
6This program is free software; you can redistribute it and/or modify
7it under the terms of the GNU General Public License version 2 as
8published by the Free Software Foundation.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18"""
19
20import sys, os
21from optparse import OptionParser, make_option
22
23from stgit.commands.common import *
24from stgit.utils import *
5e888f30 25from stgit.out import *
ca216016 26from stgit.run import *
4d0ba818
KH
27from stgit import stack, git
28
ca216016 29help = 'StGit-ify any git commits made on top of your StGit stack'
4d0ba818
KH
30usage = """%prog [options]
31
ca216016
KH
32"assimilate" will repair three kinds of inconsistencies in your StGit
33stack, all of them caused by using plain git commands on the branch:
4d0ba818 34
ca216016
KH
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
51Note that these are "inconsistencies", not "errors"; furthermore,
52"assimilate" will repair them reliably. As long as you are satisfied
53with the way "assimilate" handles them, you have no reason to avoid
54causing them in the first place if that is convenient for you."""
4d0ba818 55
6dd8fafa 56directory = DirectoryHasRepository()
4d0ba818
KH
57options = []
58
ca216016
KH
59class Commit(object):
60 def __init__(self, id):
61 self.id = id
62 self.parents = set()
63 self.children = set()
64 self.patch = None
65 self.__commit = None
66 def __get_commit(self):
67 if not self.__commit:
68 self.__commit = git.get_commit(self.id)
69 return self.__commit
70 commit = property(__get_commit)
71 def __str__(self):
72 if self.patch:
73 return '%s (%s)' % (self.id, self.patch)
74 else:
75 return self.id
76 def __repr__(self):
77 return '<%s>' % str(self)
78
79def read_commit_dag(branch):
80 out.start('Reading commit DAG')
81 commits = {}
82 patches = set()
83 for line in Run('git-rev-list', '--parents', '--all').output_lines():
84 cs = line.split()
85 for id in cs:
86 if not id in commits:
87 commits[id] = Commit(id)
88 for id in cs[1:]:
89 commits[cs[0]].parents.add(commits[id])
90 commits[id].children.add(commits[cs[0]])
91 for line in Run('git-show-ref').output_lines():
92 id, ref = line.split()
93 m = re.match(r'^refs/patches/%s/(.+)$' % branch, ref)
2b049e12 94 if m and not m.group(1).endswith('.log'):
ca216016
KH
95 c = commits[id]
96 c.patch = m.group(1)
97 patches.add(c)
98 out.done()
99 return commits, patches
100
4d0ba818
KH
101def func(parser, options, args):
102 """Assimilate a number of patches.
103 """
104
105 def nothing_to_do():
27ac2b7e 106 out.info('No commits to assimilate')
4d0ba818 107
ca216016
KH
108 orig_applied = crt_series.get_applied()
109 orig_unapplied = crt_series.get_unapplied()
4d0ba818 110
ca216016
KH
111 # If head == top, we're done.
112 head = git.get_commit(git.get_head()).get_id_hash()
113 top = crt_series.get_current_patch()
114 if top and head == top.get_top():
4d0ba818
KH
115 return nothing_to_do()
116
117 if crt_series.get_protected():
118 raise CmdException(
ca216016
KH
119 'This branch is protected. Modification is not permitted.')
120
121 # Find commits to assimilate, and applied patches.
122 commits, patches = read_commit_dag(crt_series.get_name())
123 c = commits[head]
124 patchify = []
125 applied = []
126 while len(c.parents) == 1:
127 parent, = c.parents
128 if c.patch:
129 applied.append(c)
130 elif not applied:
131 patchify.append(c)
132 c = parent
133 applied.reverse()
134 patchify.reverse()
135
136 # Find patches hidden behind a merge.
137 merge = c
138 todo = set([c])
139 seen = set()
140 hidden = set()
141 while todo:
142 c = todo.pop()
143 seen.add(c)
144 todo |= c.parents - seen
145 if c.patch:
146 hidden.add(c)
147 if hidden:
148 out.warn(('%d patch%s are hidden below the merge commit'
149 % (len(hidden), ['es', ''][len(hidden) == 1])),
150 '%s,' % merge.id, 'and will be considered unapplied.')
151
152 # Assimilate any linear sequence of commits on top of a patch.
153 names = set(p.patch for p in patches)
4d0ba818 154 def name_taken(name):
ca216016
KH
155 return name in names
156 if applied and patchify:
157 out.start('Creating %d new patch%s'
158 % (len(patchify), ['es', ''][len(patchify) == 1]))
159 for p in patchify:
160 name = make_patch_name(p.commit.get_log(), name_taken)
161 out.info('Creating patch %s from commit %s' % (name, p.id))
162 aname, amail, adate = name_email_date(p.commit.get_author())
163 cname, cmail, cdate = name_email_date(p.commit.get_committer())
164 parent, = p.parents
165 crt_series.new_patch(
166 name, can_edit = False, commit = False,
167 top = p.id, bottom = parent.id, message = p.commit.get_log(),
168 author_name = aname, author_email = amail, author_date = adate,
169 committer_name = cname, committer_email = cmail)
170 p.patch = name
171 applied.append(p)
172 names.add(name)
173 out.done()
174
175 # Write the applied/unapplied files.
176 out.start('Checking patch appliedness')
2b049e12 177 unapplied = patches - set(applied)
ca216016 178 applied_name_set = set(p.patch for p in applied)
2b049e12
KH
179 unapplied_name_set = set(p.patch for p in unapplied)
180 patches_name_set = set(p.patch for p in patches)
181 orig_patches = orig_applied + orig_unapplied
182 orig_applied_name_set = set(orig_applied)
183 orig_unapplied_name_set = set(orig_unapplied)
184 orig_patches_name_set = set(orig_patches)
185 for name in orig_patches_name_set - patches_name_set:
186 out.info('%s is gone' % name)
187 for name in applied_name_set - orig_applied_name_set:
188 out.info('%s is now applied' % name)
189 for name in unapplied_name_set - orig_unapplied_name_set:
190 out.info('%s is now unapplied' % name)
191 orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
192 def patchname_cmp(p1, p2):
193 i1 = orig_order.get(p1, len(orig_order))
194 i2 = orig_order.get(p2, len(orig_order))
195 return cmp((i1, p1), (i2, p2))
ca216016 196 crt_series.set_applied(p.patch for p in applied)
2b049e12 197 crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
ca216016 198 out.done()