--- a/vcssync/mozvcssync/gitutil.py
+++ b/vcssync/mozvcssync/gitutil.py
@@ -1,21 +1,25 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Utility functions for performing various Git functionality."""
from __future__ import absolute_import, unicode_literals
+import contextlib
import logging
import os
import pipes
import subprocess
+import dulwich.repo
+
+
logger = logging.getLogger(__name__)
class GitCommand(object):
"""Helper class for running git commands"""
def __init__(self, repo_path, secret=None):
"""
@@ -68,26 +72,30 @@ def update_git_refs(repo, reason, *actio
Accepts a ``dulwich.repo.Repo``, a bytes ``reason`` describing why this
was done, and 1 or more tuples describing the update to perform. Tuples
have the form:
('update', ref, new_id, old_id)
('create', ref, id)
('delete', ref, old_id)
('force-delete', ref)
+ ('force-update', ref, new_id)
"""
assert isinstance(reason, bytes)
commands = []
for action in actions:
if action[0] == 'update':
# Destructing will raise if length isn't correct, which is
# desired for error checking.
cmd, ref, new, old = action
commands.append(b'update %s\0%s\0%s' % (ref, new, old))
+ elif action[0] == 'force-update':
+ cmd, ref, new = action
+ commands.append(b'update %s\0%s' % (ref, new))
elif action[0] == 'create':
cmd, ref, new = action
commands.append(b'create %s\0%s' % (ref, new))
elif action[0] == 'delete':
cmd, ref, old = action
commands.append(b'delete %s\0%s' % (ref, old))
elif action[0] == 'force-delete':
cmd, ref = action
@@ -103,8 +111,54 @@ def update_git_refs(repo, reason, *actio
for command in commands:
p.stdin.write(command)
p.stdin.write(b'\0')
p.stdin.close()
res = p.wait()
# TODO could use a more rich exception type that captures output.
if res:
raise Exception('failed to update git refs')
+
+
+@contextlib.contextmanager
+def temporary_git_refs_mutations(repo, reflog_message=b'restore repo state'):
+ """Temporarily mutate Git refs during an active context manager.
+
+ When entered, the context manager takes a "snapshot" of the state of the
+ Git refs. When it exits, the Git refs are returned to their previous state.
+
+ The reflog records mutations that occurred. So any changes made when the
+ context manager is active can still be accessed until the reflog expires.
+
+ The ``repo`` argument can be a filesystem path to a repo or a
+ ``dulwich.repo.Repo`` instance.
+ """
+ if not isinstance(repo, dulwich.repo.Repo):
+ repo = dulwich.repo.Repo(repo)
+
+ old_refs = repo.get_refs()
+
+ try:
+ yield repo
+ finally:
+ new_refs = repo.get_refs()
+
+ old_keys = set(old_refs.keys())
+ new_keys = set(new_refs.keys())
+
+ actions = []
+
+ # Delete added refs.
+ for ref in sorted(new_keys - old_keys):
+ actions.append(('force-delete', ref))
+
+ # Restore deleted refs.
+ for ref in sorted(old_keys - new_keys):
+ actions.append(('create', ref, old_refs[ref]))
+
+ # Move any changed refs.
+ for ref in sorted(old_keys & new_keys):
+ if old_refs[ref] == new_refs[ref]:
+ continue
+
+ actions.append(('force-update', ref, old_refs[ref]))
+
+ update_git_refs(repo, reflog_message, *actions)