chiark / gitweb /
43672fd5bab022bb8f1d809659c6d91684d82e79
[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 and not m.group(1).endswith('.log'):
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     unapplied = patches - set(applied)
177     applied_name_set = set(p.patch for p in applied)
178     unapplied_name_set = set(p.patch for p in unapplied)
179     patches_name_set = set(p.patch for p in patches)
180     orig_patches = orig_applied + orig_unapplied
181     orig_applied_name_set = set(orig_applied)
182     orig_unapplied_name_set = set(orig_unapplied)
183     orig_patches_name_set = set(orig_patches)
184     for name in orig_patches_name_set - patches_name_set:
185         out.info('%s is gone' % name)
186     for name in applied_name_set - orig_applied_name_set:
187         out.info('%s is now applied' % name)
188     for name in unapplied_name_set - orig_unapplied_name_set:
189         out.info('%s is now unapplied' % name)
190     orig_order = dict(zip(orig_patches, xrange(len(orig_patches))))
191     def patchname_cmp(p1, p2):
192         i1 = orig_order.get(p1, len(orig_order))
193         i2 = orig_order.get(p2, len(orig_order))
194         return cmp((i1, p1), (i2, p2))
195     crt_series.set_applied(p.patch for p in applied)
196     crt_series.set_unapplied(sorted(unapplied_name_set, cmp = patchname_cmp))
197     out.done()