Stack metadata version 4
[stgit.git] / stgit / lib / stackupgrade.py
blob683701a1853509e557b3d68ac1f5d583b9fd275d
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 mkdir_file(filename, mode, encoding='utf-8'):
19 """Opens filename with the given mode, creating the directory it's
20 in if it doesn't already exist."""
21 os.makedirs(os.path.dirname(filename), exist_ok=True)
22 return open(filename, mode, encoding=encoding)
25 def read_strings(filename, encoding='utf-8'):
26 """Reads the lines from a file"""
27 with open(filename, encoding=encoding) as f:
28 return [line.strip() for line in f.readlines()]
31 def read_string(filename, encoding='utf-8'):
32 """Reads the first line from a file"""
33 with open(filename, encoding=encoding) as f:
34 return f.readline().strip()
37 def create_empty_file(name):
38 """Creates an empty file"""
39 mkdir_file(name, 'w+').close()
42 def update_to_current_format_version(repository, branch):
43 """Update a potentially older StGit directory structure to the latest
44 version. Note: This function should depend as little as possible
45 on external functions that may change during a format version
46 bump, since it must remain able to process older formats."""
48 patches_dir = os.path.join(repository.directory, 'patches')
49 branch_dir = os.path.join(patches_dir, branch)
50 old_format_key = format_version_key(branch)
51 older_format_key = 'branch.%s.stgitformatversion' % branch
53 def get_meta_file_version():
54 stack_ref = 'refs/heads/%s.stgit:meta' % branch
55 try:
56 lines = (
57 repository.run(['git', 'show', stack_ref])
58 .discard_stderr()
59 .output_lines()
61 except RunException:
62 return None
64 for line in lines:
65 if line.startswith('Version: '):
66 return int(line.split('Version: ', 1)[1])
67 else:
68 return None
70 def get_format_version():
71 """Return the integer format version number, or None if the
72 branch doesn't have any StGit metadata at all, of any version."""
73 mfv = get_meta_file_version()
74 if mfv is not None and mfv >= 4:
75 # Modern-era format version found in branch meta blob.
76 return mfv
78 # Older format versions were stored in the Git config.
79 fv = config.get(old_format_key)
80 ofv = config.get(older_format_key)
81 if fv:
82 # Great, there's an explicitly recorded format version
83 # number, which means that the branch is initialized and
84 # of that exact version.
85 return int(fv)
86 elif ofv:
87 # Old name for the version info: upgrade it.
88 config.set(old_format_key, ofv)
89 config.unset(older_format_key)
90 return int(ofv)
91 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
92 # There's a .git/patches/<branch>/patches dirctory, which
93 # means this is an initialized version 1 branch.
94 return 1
95 elif os.path.isdir(branch_dir):
96 # There's a .git/patches/<branch> directory, which means
97 # this is an initialized version 0 branch.
98 return 0
99 else:
100 # The branch doesn't seem to be initialized at all.
101 return None
103 def set_format_version_in_config(v):
104 out.info('Upgraded branch %s to format version %d' % (branch, v))
105 config.set(old_format_key, '%d' % v)
107 def mkdir(d):
108 if not os.path.isdir(d):
109 os.makedirs(d)
111 def rm(f):
112 if os.path.exists(f):
113 os.remove(f)
115 def rm_ref(ref):
116 if repository.refs.exists(ref):
117 repository.refs.delete(ref)
119 # Update 0 -> 1.
120 if get_format_version() == 0:
121 mkdir(os.path.join(branch_dir, 'trash'))
122 patch_dir = os.path.join(branch_dir, 'patches')
123 mkdir(patch_dir)
124 refs_base = 'refs/patches/%s' % branch
125 with open(os.path.join(branch_dir, 'unapplied')) as f:
126 patches = f.readlines()
127 with open(os.path.join(branch_dir, 'applied')) as f:
128 patches.extend(f.readlines())
129 for patch in patches:
130 patch = patch.strip()
131 os.rename(os.path.join(branch_dir, patch), os.path.join(patch_dir, patch))
132 topfield = os.path.join(patch_dir, patch, 'top')
133 if os.path.isfile(topfield):
134 top = read_string(topfield)
135 else:
136 top = None
137 if top:
138 repository.refs.set(
139 refs_base + '/' + patch,
140 repository.get_commit(top),
141 'StGit upgrade',
143 set_format_version_in_config(1)
145 # Update 1 -> 2.
146 if get_format_version() == 1:
147 desc_file = os.path.join(branch_dir, 'description')
148 if os.path.isfile(desc_file):
149 desc = read_string(desc_file)
150 if desc:
151 config.set('branch.%s.description' % branch, desc)
152 rm(desc_file)
153 rm(os.path.join(branch_dir, 'current'))
154 rm_ref('refs/bases/%s' % branch)
155 set_format_version_in_config(2)
157 # Update 2 -> 3
158 if get_format_version() == 2:
159 protect_file = os.path.join(branch_dir, 'protected')
160 if os.path.isfile(protect_file):
161 config.set('branch.%s.stgit.protect' % branch, 'true')
162 os.remove(protect_file)
163 set_format_version_in_config(3)
165 # compatibility with the new infrastructure. The changes here do not
166 # affect the compatibility with the old infrastructure (format version 2)
167 if get_format_version() == 3:
168 hidden_file = os.path.join(branch_dir, 'hidden')
169 if not os.path.isfile(hidden_file):
170 create_empty_file(hidden_file)
172 applied_file = os.path.join(branch_dir, 'applied')
173 unapplied_file = os.path.join(branch_dir, 'unapplied')
175 applied = read_strings(applied_file)
176 unapplied = read_strings(unapplied_file)
177 hidden = read_strings(hidden_file)
179 state_ref = 'refs/heads/%s.stgit' % branch
181 head = repository.refs.get('refs/heads/%s' % branch)
182 parents = [head]
183 meta_lines = [
184 'Version: 4',
185 'Previous: None',
186 'Head: %s' % head.sha1,
189 patches_tree = {}
191 for patch_list, title in [
192 (applied, 'Applied'),
193 (unapplied, 'Unapplied'),
194 (hidden, 'Hidden'),
196 meta_lines.append('%s:' % title)
197 for i, pn in enumerate(patch_list):
198 patch_ref = 'refs/patches/%s/%s' % (branch, pn)
199 commit = repository.refs.get(patch_ref)
200 meta_lines.append(' %s: %s' % (pn, commit.sha1))
201 if title != 'Applied' or i == len(patch_list) - 1:
202 if commit not in parents:
203 parents.append(commit)
204 cd = commit.data
205 patch_meta = '\n'.join(
207 'Bottom: %s' % cd.parent.data.tree.sha1,
208 'Top: %s' % cd.tree.sha1,
209 'Author: %s' % cd.author.name_email,
210 'Date: %s' % cd.author.date,
212 cd.message_str,
214 ).encode('utf-8')
215 patches_tree[pn] = repository.commit(BlobData(patch_meta))
216 meta_lines.append('')
218 meta = '\n'.join(meta_lines).encode('utf-8')
219 tree = repository.commit(
220 TreeData(
222 'meta': repository.commit(BlobData(meta)),
223 'patches': repository.commit(TreeData(patches_tree)),
227 state_commit = repository.commit(
228 CommitData(
229 tree=tree,
230 message='stack upgrade to version 4',
231 parents=parents,
234 repository.refs.set(state_ref, state_commit, 'stack upgrade to v4')
236 for patch_list in [applied, unapplied, hidden]:
237 for pn in patch_list:
238 patch_log_ref = 'refs/patches/%s/%s.log' % (branch, pn)
239 if repository.refs.exists(patch_log_ref):
240 repository.refs.delete(patch_log_ref)
242 config.unset(old_format_key)
244 shutil.rmtree(branch_dir)
245 try:
246 # .git/patches will be removed after the last stack is converted
247 os.rmdir(patches_dir)
248 except OSError:
249 pass
250 out.info('Upgraded branch %s to format version %d' % (branch, 4))
252 # Make sure we're at the latest version.
253 fv = get_format_version()
254 if fv not in [None, FORMAT_VERSION]:
255 raise StackException(
256 'Branch %s is at format version %d, expected %d'
257 % (branch, fv, FORMAT_VERSION)
259 return fv is not None # true if branch is initialized