| 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() |