Skip to content

Commit 7641796

Browse files
committed
Stack metadata version 5
Stack metadata is now kept in refs/stacks/<branch> instead of refs/heads/<branch>.stgit. The rationale: - StGit's metadata branch is an implementation detail that leaks to users whenever git branches are inspected. - `git log --branches` and other views of the commit DAG can become extremely cluttered when the stack metadata branch enters the picture. Avoiding refs/heads solves this problem. - There is a nice symmetry between refs/patches/<branch> and refs/heads/<branch>. - Distributed use (push, pull, clone) of stack metadata is not significantly different with the new ref. And the stack metadata file format is now JSON instead of the former custom format. - Can use off-the-shelf JSON parser and serializer from Python standard library. Fast, easy, and less surface area for bugs. - Stack metadata becomes more accessible to third-party tools, scripts, and programming languages. - JSON format allows for additional metadata to be associated with each patch and/or the entire stack. This may be useful for advanced features such as patch guards. - The stack.json file remains at least as human-readable as the old meta file. Line-oriented diffs also remain effective. Resolves #65. Signed-off-by: Peter Grayson <[email protected]>
1 parent 21c09b7 commit 7641796

11 files changed

+179
-98
lines changed

completion/stgit.zsh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ __stg_git_describe_branch () {
709709
__stg_branch_stgit() {
710710
declare -a stg_branches
711711
stg_branches=(
712-
${(u)${${${(f)"$(_call_program branchrefs git for-each-ref --format='"%(refname)"' refs/heads 2>/dev/null)"}#refs/heads/}:/*.stgit/}}
712+
${${(f)"$(_call_program branchrefs git for-each-ref --format='"%(refname)"' refs/stacks 2>/dev/null)"}#refs/stacks/}
713713
)
714714
local expl
715715
_wanted -V branches expl "branch" compadd $stg_branches

stgit/commands/branch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def func(parser, options, args):
404404
branch_names = sorted(
405405
ref.replace('refs/heads/', '', 1)
406406
for ref in repository.refs
407-
if ref.startswith('refs/heads/') and not ref.endswith('.stgit')
407+
if ref.startswith('refs/heads/')
408408
)
409409

410410
if branch_names:

stgit/lib/log.py

Lines changed: 54 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
A stack log is a Git branch. Each commit contains the complete state (metadata) of the
44
stack at the moment it was written; the most recent commit has the most recent state.
55
6-
For a branch `foo`, the stack state log is stored in the branch `foo.stgit`.
6+
For a branch `foo`, the stack state log is stored in the ref `refs/stacks/foo`.
77
88
Each log entry makes sure to have proper references to everything it needs to make it
99
safe against garbage collection. The stack state log can even be pulled from one
@@ -89,17 +89,23 @@
8989
Format version 4
9090
================
9191
92-
The metadata in the `<branch>.stgit` branch is the same as format version 1.
92+
The metadata in `refs/heads/<branch>.stgit` branch is the same as format version 1.
9393
9494
Format version 4 indicates that, unlike previous format versions used by older versions
9595
of StGit, the stack log state is *only* contained in the stack log branch and *not* as
9696
files in the .git/patches directory.
9797
98+
Format version 5
99+
================
100+
101+
Stack metadata resides in `refs/stacks/<branch>` where the `stack.json` metadata file
102+
is JSON format instead of the previous custom format.
103+
98104
"""
99105

106+
import json
100107
import re
101108

102-
from stgit import utils
103109
from stgit.exception import StgException
104110
from stgit.lib.git import BlobData, CommitData, TreeData
105111
from stgit.lib.stackupgrade import FORMAT_VERSION
@@ -185,68 +191,47 @@ def from_stack(cls, prev, stack):
185191
patches={pn: stack.patches[pn] for pn in stack.patchorder.all},
186192
)
187193

188-
@staticmethod
189-
def _parse_metadata(repo, metadata):
190-
"""Parse a stack log metadata string."""
191-
if not metadata.startswith('Version:'):
192-
raise LogParseException('Malformed log metadata')
193-
metadata = metadata.splitlines()
194-
version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
195-
try:
196-
version = int(version_str)
197-
except ValueError:
198-
raise LogParseException('Malformed version number: %r' % version_str)
199-
if version < FORMAT_VERSION:
200-
raise LogException('Log is version %d, which is too old' % version)
201-
if version > FORMAT_VERSION:
202-
raise LogException('Log is version %d, which is too new' % version)
203-
parsed = {}
204-
key = None
205-
for line in metadata:
206-
if line.startswith(' '):
207-
assert key is not None
208-
parsed[key].append(line.strip())
209-
else:
210-
key, val = [x.strip() for x in line.split(':', 1)]
211-
if val:
212-
parsed[key] = val
213-
else:
214-
parsed[key] = []
215-
prev = parsed['Previous']
216-
if prev == 'None':
217-
prev = None
218-
else:
219-
prev = repo.get_commit(prev)
220-
head = repo.get_commit(parsed['Head'])
221-
lists = {'Applied': [], 'Unapplied': [], 'Hidden': []}
222-
patches = {}
223-
for lst in lists:
224-
for entry in parsed[lst]:
225-
pn, sha1 = [x.strip() for x in entry.split(':')]
226-
lists[lst].append(pn)
227-
patches[pn] = repo.get_commit(sha1)
228-
return (
229-
prev,
230-
head,
231-
lists['Applied'],
232-
lists['Unapplied'],
233-
lists['Hidden'],
234-
patches,
235-
)
236-
237194
@classmethod
238195
def from_commit(cls, repo, commit):
239196
"""Parse a (full or simplified) stack log commit."""
240197
try:
241-
perm, meta_blob = commit.data.tree.data['meta']
198+
perm, stack_json_blob = commit.data.tree.data['stack.json']
242199
except KeyError:
243200
raise LogParseException('Not a stack log')
244201

245-
prev, head, applied, unapplied, hidden, patches = cls._parse_metadata(
246-
repo, meta_blob.data.bytes.decode('utf-8')
247-
)
202+
try:
203+
stack_json = json.loads(stack_json_blob.data.bytes)
204+
except json.JSONDecodeError as e:
205+
raise LogParseException(str(e))
206+
207+
version = stack_json.get('version')
208+
209+
if version is None:
210+
raise LogException('Missing stack metadata version')
211+
elif version < FORMAT_VERSION:
212+
raise LogException('Log is version %d, which is too old' % version)
213+
elif version > FORMAT_VERSION:
214+
raise LogException('Log is version %d, which is too new' % version)
215+
216+
patches = {
217+
pn: repo.get_commit(patch_info['oid'])
218+
for pn, patch_info in stack_json['patches'].items()
219+
}
248220

249-
return cls(prev, head, applied, unapplied, hidden, patches, commit)
221+
if stack_json['prev'] is None:
222+
prev = None
223+
else:
224+
prev = repo.get_commit(stack_json['prev'])
225+
226+
return cls(
227+
prev,
228+
repo.get_commit(stack_json['head']),
229+
stack_json['applied'],
230+
stack_json['unapplied'],
231+
stack_json['hidden'],
232+
patches,
233+
commit,
234+
)
250235

251236
def _parents(self, prev_state):
252237
"""Parents this entry needs to be a descendant of all commits it refers to."""
@@ -258,23 +243,18 @@ def _parents(self, prev_state):
258243
xp -= set(prev_state.patches.values())
259244
return xp
260245

261-
def _metadata_blob(self, repo, prev_state):
262-
lines = ['Version: %d' % FORMAT_VERSION]
263-
lines.append(
264-
'Previous: %s' % ('None' if prev_state is None else prev_state.commit.sha1)
246+
def _stack_json_blob(self, repo, prev_state):
247+
stack_json = dict(
248+
version=FORMAT_VERSION,
249+
prev=None if prev_state is None else prev_state.commit.sha1,
250+
head=self.head.sha1,
251+
applied=self.applied,
252+
unapplied=self.unapplied,
253+
hidden=self.hidden,
254+
patches={pn: dict(oid=patch.sha1) for pn, patch in self.patches.items()},
265255
)
266-
lines.append('Head: %s' % self.head.sha1)
267-
for patch_list, title in [
268-
(self.applied, 'Applied'),
269-
(self.unapplied, 'Unapplied'),
270-
(self.hidden, 'Hidden'),
271-
]:
272-
lines.append('%s:' % title)
273-
for pn in patch_list:
274-
lines.append(' %s: %s' % (pn, self.patches[pn].sha1))
275-
lines.append('')
276-
metadata_str = '\n'.join(lines)
277-
return repo.commit(BlobData(metadata_str.encode('utf-8')))
256+
blob = json.dumps(stack_json, indent=2).encode('utf-8')
257+
return repo.commit(BlobData(blob))
278258

279259
def _patch_blob(self, repo, pn, commit, prev_state):
280260
if prev_state is not None:
@@ -308,7 +288,7 @@ def _tree(self, repo, prev_state):
308288
return repo.commit(
309289
TreeData(
310290
{
311-
'meta': self._metadata_blob(repo, prev_state),
291+
'stack.json': self._stack_json_blob(repo, prev_state),
312292
'patches': self._patches_tree(repo, prev_state),
313293
}
314294
)

stgit/lib/stack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def _stack_state_ref(stack_name):
1212
"""Reference to stack state metadata. A.k.a. the stack's "log"."""
13-
return 'refs/heads/%s.stgit' % (stack_name,)
13+
return 'refs/stacks/%s' % (stack_name,)
1414

1515

1616
def _patch_ref(stack_name, patch_name):

stgit/lib/stackupgrade.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import shutil
34

@@ -8,7 +9,7 @@
89
from stgit.run import RunException
910

1011
# The current StGit metadata format version.
11-
FORMAT_VERSION = 4
12+
FORMAT_VERSION = 5
1213

1314

1415
def _format_version_key(branch):
@@ -47,6 +48,16 @@ def update_to_current_format_version(repository, branch):
4748
older_format_key = 'branch.%s.stgitformatversion' % branch
4849

4950
def get_meta_file_version():
51+
"""Get format version from the ``meta`` file in the stack log branch."""
52+
new_version = get_stack_json_file_version()
53+
if new_version is not None:
54+
return new_version
55+
56+
old_version = get_old_meta_file_version()
57+
if old_version is not None:
58+
return old_version
59+
60+
def get_old_meta_file_version():
5061
"""Get format version from the ``meta`` file in the stack log branch."""
5162
stack_ref = 'refs/heads/%s.stgit:meta' % branch
5263
try:
@@ -64,6 +75,25 @@ def get_meta_file_version():
6475
else:
6576
return None
6677

78+
def get_stack_json_file_version():
79+
stack_ref = 'refs/stacks/%s:stack.json' % branch
80+
try:
81+
data = (
82+
repository.run(['git', 'show', stack_ref])
83+
.decoding(None)
84+
.discard_stderr()
85+
.raw_output()
86+
)
87+
except RunException:
88+
return None
89+
90+
try:
91+
stack_json = json.loads(data)
92+
except json.JSONDecodeError:
93+
return None
94+
else:
95+
return stack_json.get('version')
96+
6797
def get_format_version():
6898
"""Return the integer format version number.
6999
@@ -243,6 +273,77 @@ def rm_ref(ref):
243273
pass
244274
out.info('Upgraded branch %s to format version %d' % (branch, 4))
245275

276+
# Metadata moves from refs/heads/<branch>.stgit to refs/stacks/<branch>.
277+
# Also, metadata file format is JSON instead of custom format.
278+
if get_format_version() == 4:
279+
old_state_ref = 'refs/heads/%s.stgit' % branch
280+
old_state = repository.refs.get(old_state_ref)
281+
old_meta = old_state.data.tree.data['meta'][1].data.bytes
282+
lines = old_meta.decode('utf-8').splitlines()
283+
if not lines[0].startswith('Version: 4'):
284+
raise StackException('Malformed metadata (expected version 4)')
285+
286+
parsed = {}
287+
key = None
288+
for line in lines:
289+
if line.startswith(' '):
290+
assert key is not None
291+
parsed[key].append(line.strip())
292+
else:
293+
key, val = [x.strip() for x in line.split(':', 1)]
294+
if val:
295+
parsed[key] = val
296+
else:
297+
parsed[key] = []
298+
299+
head = repository.refs.get('refs/heads/%s' % branch)
300+
301+
new_meta = dict(
302+
version=5,
303+
prev=parsed['Previous'],
304+
head=head.sha1,
305+
applied=[],
306+
unapplied=[],
307+
hidden=[],
308+
patches=dict(),
309+
)
310+
311+
if parsed['Head'] != new_meta['head']:
312+
raise StackException('Unexpected head mismatch')
313+
314+
for patch_list_name in ['Applied', 'Unapplied', 'Hidden']:
315+
for entry in parsed[patch_list_name]:
316+
pn, sha1 = [x.strip() for x in entry.split(':')]
317+
new_patch_list_name = patch_list_name.lower()
318+
new_meta[new_patch_list_name].append(pn)
319+
new_meta['patches'][pn] = dict(oid=sha1)
320+
321+
meta_bytes = json.dumps(new_meta, indent=2).encode('utf-8')
322+
323+
tree = repository.commit(
324+
TreeData(
325+
{
326+
'stack.json': repository.commit(BlobData(meta_bytes)),
327+
'patches': old_state.data.tree.data['patches'],
328+
}
329+
)
330+
)
331+
332+
repository.refs.set(
333+
'refs/stacks/%s' % branch,
334+
repository.commit(
335+
CommitData(
336+
tree=tree,
337+
message='stack upgrade to version 5',
338+
parents=[head],
339+
)
340+
),
341+
'stack upgrade to v5',
342+
)
343+
344+
repository.refs.delete(old_state_ref)
345+
out.info('Upgraded branch %s to format version %d' % (branch, 5))
346+
246347
# Make sure we're at the latest version.
247348
fv = get_format_version()
248349
if fv not in [None, FORMAT_VERSION]:

t/t1000-branch-create.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ test_expect_success \
3333

3434
test_expect_success \
3535
'Check for various bits of new branch' '
36-
test_path_is_file .git/refs/heads/new &&
37-
test_path_is_file .git/refs/heads/new.stgit &&
36+
git show-ref --verify --quiet refs/heads/new &&
37+
git show-ref --verify --quiet refs/stacks/new &&
3838
test_path_is_missing .git/patches/new &&
3939
test_path_is_missing .git/refs/patches &&
4040
test "$(git config --get branch.new.remote)" = "origin" &&

t/t1001-branch-rename.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ Exercises branch renaming commands.
1212

1313
_assert_branch_exists() {
1414
git config --get-regexp "branch\\.$1\\." &&
15-
test_path_is_file ".git/refs/heads/$1" &&
16-
test_path_is_file ".git/refs/heads/$1.stgit"
15+
git show-ref --verify --quiet "refs/heads/$1" &&
16+
git show-ref --verify --quiet "refs/stacks/$1"
1717
}
1818

1919
_assert_branch_missing() {
2020
test_expect_code 1 git config --get-regexp "branch\\.$1\\." &&
21-
test_path_is_missing ".git/refs/heads/$1" &&
22-
test_path_is_missing ".git/refs/heads/$1.stgit"
21+
! git show-ref --verify --quiet "refs/heads/$1" &&
22+
! git show-ref --verify --quiet "refs/stacks/$1"
2323
}
2424

2525
_assert_current_branch_name() {

t/t1009-gpg.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test_expect_success GPG \
1010
git config commit.gpgsign true
1111
git config user.signingkey ${GIT_COMMITTER_EMAIL}
1212
stg init &&
13-
git verify-commit master.stgit
13+
git verify-commit refs/stacks/master
1414
'
1515

1616
test_expect_success GPG \

t/t2110-pull-stack.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test_expect_success 'Pull master and stack with all applied' '
1818
test_create_repo cloned &&
1919
(cd cloned &&
2020
git config pull.ff only &&
21-
git pull -f ../upstream master:master master.stgit:master.stgit &&
21+
git pull -f ../upstream master:master refs/stacks/master:refs/stacks/master &&
2222
[ "$(echo $(stg series --applied --noprefix))" = "patch-1 patch-2 patch-3" ]
2323
)
2424
'
@@ -28,7 +28,7 @@ test_expect_success 'Pull master and stack with unapplied patches' '
2828
stg pop
2929
) &&
3030
(cd cloned &&
31-
git pull -f ../upstream master:master master.stgit:master.stgit &&
31+
git pull -f ../upstream master:master refs/stacks/master:refs/stacks/master &&
3232
[ "$(echo $(stg series --applied --noprefix))" = "patch-1 patch-2" ] &&
3333
[ "$(echo $(stg series --unapplied --noprefix))" = "patch-3" ]
3434
)

0 commit comments

Comments
 (0)