stg import now extracts Message-ID header
[stgit.git] / stgit / lib / stackupgrade.py
blobd1f113f7d6f8d2308b5cfa2151d9230f70a1c925
1 import os
2 import shutil
4 from stgit.config import config
5 from stgit.exception import StackException
6 from stgit.lib.git.objects import BlobData, CommitData, TreeData
7 from stgit.out import out
8 from stgit.run import RunException
10 # The current StGit metadata format version.
11 FORMAT_VERSION = 4
14 def _format_version_key(branch):
15 return 'branch.%s.stgit.stackformatversion' % branch
18 def _read_strings(filename):
19 """Reads the lines from a file"""
20 with open(filename, encoding='utf-8') as f:
21 return [line.strip() for line in f.readlines()]
24 def _read_string(filename):
25 """Reads the first line from a file"""
26 with open(filename, encoding='utf-8') as f:
27 return f.readline().strip()
30 def _try_rm(f):
31 if os.path.exists(f):
32 os.remove(f)
35 def update_to_current_format_version(repository, branch):
36 """Update a potentially older StGit directory structure to the latest version.
38 Note: This function should depend as little as possible on external functions that
39 may change during a format version bump, since it must remain able to process older
40 formats.
42 """
44 patches_dir = os.path.join(repository.directory, 'patches')
45 branch_dir = os.path.join(patches_dir, branch)
46 old_format_key = _format_version_key(branch)
47 older_format_key = 'branch.%s.stgitformatversion' % branch
49 def get_meta_file_version():
50 """Get format version from the ``meta`` file in the stack log branch."""
51 stack_ref = 'refs/heads/%s.stgit:meta' % branch
52 try:
53 lines = (
54 repository.run(['git', 'show', stack_ref])
55 .discard_stderr()
56 .output_lines()
58 except RunException:
59 return None
61 for line in lines:
62 if line.startswith('Version: '):
63 return int(line.split('Version: ', 1)[1])
64 else:
65 return None
67 def get_format_version():
68 """Return the integer format version number.
70 :returns: the format version number or None if the branch does not have any
71 StGit metadata at all, of any version
73 """
74 mfv = get_meta_file_version()
75 if mfv is not None and mfv >= 4:
76 # Modern-era format version found in branch meta blob.
77 return mfv
79 # Older format versions were stored in the Git config.
80 fv = config.get(old_format_key)
81 ofv = config.get(older_format_key)
82 if fv:
83 # Great, there's an explicitly recorded format version
84 # number, which means that the branch is initialized and
85 # of that exact version.
86 return int(fv)
87 elif ofv:
88 # Old name for the version info: upgrade it.
89 config.set(old_format_key, ofv)
90 config.unset(older_format_key)
91 return int(ofv)
92 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
93 # There's a .git/patches/<branch>/patches dirctory, which
94 # means this is an initialized version 1 branch.
95 return 1
96 elif os.path.isdir(branch_dir):
97 # There's a .git/patches/<branch> directory, which means
98 # this is an initialized version 0 branch.
99 return 0
100 else:
101 # The branch doesn't seem to be initialized at all.
102 return None
104 def set_format_version_in_config(v):
105 out.info('Upgraded branch %s to format version %d' % (branch, v))
106 config.set(old_format_key, '%d' % v)
108 def rm_ref(ref):
109 if repository.refs.exists(ref):
110 repository.refs.delete(ref)
112 # Update 0 -> 1.
113 if get_format_version() == 0:
114 os.makedirs(os.path.join(branch_dir, 'trash'), exist_ok=True)
115 patch_dir = os.path.join(branch_dir, 'patches')
116 os.makedirs(patch_dir, exist_ok=True)
117 refs_base = 'refs/patches/%s' % branch
118 with open(os.path.join(branch_dir, 'unapplied')) as f:
119 patches = f.readlines()
120 with open(os.path.join(branch_dir, 'applied')) as f:
121 patches.extend(f.readlines())
122 for patch in patches:
123 patch = patch.strip()
124 os.rename(os.path.join(branch_dir, patch), os.path.join(patch_dir, patch))
125 topfield = os.path.join(patch_dir, patch, 'top')
126 if os.path.isfile(topfield):
127 top = _read_string(topfield)
128 else:
129 top = None
130 if top:
131 repository.refs.set(
132 refs_base + '/' + patch,
133 repository.get_commit(top),
134 'StGit upgrade',
136 set_format_version_in_config(1)
138 # Update 1 -> 2.
139 if get_format_version() == 1:
140 desc_file = os.path.join(branch_dir, 'description')
141 if os.path.isfile(desc_file):
142 desc = _read_string(desc_file)
143 if desc:
144 config.set('branch.%s.description' % branch, desc)
145 _try_rm(desc_file)
146 _try_rm(os.path.join(branch_dir, 'current'))
147 rm_ref('refs/bases/%s' % branch)
148 set_format_version_in_config(2)
150 # Update 2 -> 3
151 if get_format_version() == 2:
152 protect_file = os.path.join(branch_dir, 'protected')
153 if os.path.isfile(protect_file):
154 config.set('branch.%s.stgit.protect' % branch, 'true')
155 os.remove(protect_file)
156 set_format_version_in_config(3)
158 # compatibility with the new infrastructure. The changes here do not
159 # affect the compatibility with the old infrastructure (format version 2)
160 if get_format_version() == 3:
161 os.makedirs(branch_dir, exist_ok=True)
162 hidden_file = os.path.join(branch_dir, 'hidden')
163 if not os.path.isfile(hidden_file):
164 open(hidden_file, 'w+', encoding='utf-8').close()
166 applied_file = os.path.join(branch_dir, 'applied')
167 unapplied_file = os.path.join(branch_dir, 'unapplied')
169 applied = _read_strings(applied_file)
170 unapplied = _read_strings(unapplied_file)
171 hidden = _read_strings(hidden_file)
173 state_ref = 'refs/heads/%s.stgit' % branch
175 head = repository.refs.get('refs/heads/%s' % branch)
176 parents = [head]
177 meta_lines = [
178 'Version: 4',
179 'Previous: None',
180 'Head: %s' % head.sha1,
183 patches_tree = {}
185 for patch_list, title in [
186 (applied, 'Applied'),
187 (unapplied, 'Unapplied'),
188 (hidden, 'Hidden'),
190 meta_lines.append('%s:' % title)
191 for i, pn in enumerate(patch_list):
192 patch_ref = 'refs/patches/%s/%s' % (branch, pn)
193 commit = repository.refs.get(patch_ref)
194 meta_lines.append(' %s: %s' % (pn, commit.sha1))
195 if title != 'Applied' or i == len(patch_list) - 1:
196 if commit not in parents:
197 parents.append(commit)
198 cd = commit.data
199 patch_meta = '\n'.join(
201 'Bottom: %s' % cd.parent.data.tree.sha1,
202 'Top: %s' % cd.tree.sha1,
203 'Author: %s' % cd.author.name_email,
204 'Date: %s' % cd.author.date,
206 cd.message_str,
208 ).encode('utf-8')
209 patches_tree[pn] = repository.commit(BlobData(patch_meta))
210 meta_lines.append('')
212 meta = '\n'.join(meta_lines).encode('utf-8')
213 tree = repository.commit(
214 TreeData(
216 'meta': repository.commit(BlobData(meta)),
217 'patches': repository.commit(TreeData(patches_tree)),
221 state_commit = repository.commit(
222 CommitData(
223 tree=tree,
224 message='stack upgrade to version 4',
225 parents=parents,
228 repository.refs.set(state_ref, state_commit, 'stack upgrade to v4')
230 for patch_list in [applied, unapplied, hidden]:
231 for pn in patch_list:
232 patch_log_ref = 'refs/patches/%s/%s.log' % (branch, pn)
233 if repository.refs.exists(patch_log_ref):
234 repository.refs.delete(patch_log_ref)
236 config.unset(old_format_key)
238 shutil.rmtree(branch_dir)
239 try:
240 # .git/patches will be removed after the last stack is converted
241 os.rmdir(patches_dir)
242 except OSError:
243 pass
244 out.info('Upgraded branch %s to format version %d' % (branch, 4))
246 # Make sure we're at the latest version.
247 fv = get_format_version()
248 if fv not in [None, FORMAT_VERSION]:
249 raise StackException(
250 'Branch %s is at format version %d, expected %d'
251 % (branch, fv, FORMAT_VERSION)
253 return fv is not None # true if branch is initialized