[damned-lies] Added cherry-picking commit capability (no UI yet)
- From: Claude Paroz <claudep src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [damned-lies] Added cherry-picking commit capability (no UI yet)
- Date: Sat, 17 Oct 2015 14:58:31 +0000 (UTC)
commit 8ea9927c9ef6c44cb58abd8d5940fb689b2d5c71
Author: Claude Paroz <claude 2xlibre net>
Date: Sat Oct 17 15:46:09 2015 +0200
Added cherry-picking commit capability (no UI yet)
Refs bug #726786.
stats/models.py | 97 +++++++++++++++++++++++++++++++++----------------
stats/tests/tests.py | 93 +++++++++++++++++++++++++++++++++++++++---------
2 files changed, 141 insertions(+), 49 deletions(-)
---
diff --git a/stats/models.py b/stats/models.py
index 6117165..93fdaa5 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -164,6 +164,7 @@ class Module(models.Model):
return True
return False
+
class ModuleLock(object):
""" Weird things happen when multiple updates run in parallel for the same module
We use filesystem directories creation/deletion to act as global lock mecanism
@@ -179,7 +180,8 @@ class ModuleLock(object):
os.mkdir(self.dirpath)
break;
except OSError:
- sleep(30)
+ # The directory exists, something is happening on the module, let's wait
+ sleep(10)
def __exit__(self, *exc_info):
os.rmdir(self.dirpath)
@@ -211,7 +213,6 @@ class Branch(models.Model):
def __init__(self, *args, **kwargs):
super(Branch, self).__init__(*args, **kwargs)
- self.checkout_lock = threading.Lock()
self._ui_stats = None
self._doc_stats = None
@@ -582,23 +583,7 @@ class Branch(models.Model):
command_list = []
if self._exists():
- # Path exists, update repos
- if vcs_type == "cvs":
- command_list.append((modulepath, ['cvs', '-z4', 'up', '-Pd']))
- elif vcs_type == "svn":
- command_list.append((modulepath, ['svn', 'up', '--non-interactive']))
- elif vcs_type == "hg":
- command_list.append((modulepath, ['hg', 'revert', '--all']))
- elif vcs_type == "git":
- # tester "git checkout %(branch)s && git clean -dfq && git pull origin/%(branch)s"
- command_list.append((modulepath, ['git', 'checkout', '-f', self.name]))
- command_list.append((modulepath, ['git', 'fetch']))
- command_list.append((modulepath, ['git', 'reset', '--hard', 'origin/%s' % self.name]))
- command_list.append((modulepath, ['git', 'clean', '-dfq']))
- # check if there are any submodules and init & update them
- command_list.append((modulepath, "if [ -e .gitmodules ]; then git submodule update --init;
fi"))
- elif vcs_type == "bzr":
- command_list.append((modulepath, ['bzr', 'up']))
+ command_list.extend(self.update_repo(execute=False))
else:
# Checkout
vcs_path = self.get_vcs_url()
@@ -633,21 +618,49 @@ class Branch(models.Model):
# Run command(s)
logging.debug("Checking '%s.%s' out to '%s'..." % (module_name, self.name, modulepath))
# Do not allow 2 checkouts to run in parallel on the same branch
- self.checkout_lock.acquire()
- try:
+ with ModuleLock(self.module):
for working_dir, command in command_list:
utils.run_shell_command(command, raise_on_error=True, cwd=working_dir)
- finally:
- self.checkout_lock.release()
return 1
- def commit_po(self, po_file, domain, language, author):
+ def update_repo(self, execute=True):
+ """
+ Update existing repository checkout.
+ WARNING: the calling method should acquire a lock for the module to not
+ mix checkouts in different threads/processes.
+ """
+ modulepath = self.co_path()
+ logging.debug("Updating '%s.%s' (in '%s')..." % (self.module.name, self.name, modulepath))
+ command_list = []
+ if self.module.vcs_type == "cvs":
+ command_list.append((modulepath, ['cvs', '-z4', 'up', '-Pd']))
+ elif self.module.vcs_type == "svn":
+ command_list.append((modulepath, ['svn', 'up', '--non-interactive']))
+ elif self.module.vcs_type == "hg":
+ command_list.append((modulepath, ['hg', 'revert', '--all']))
+ elif self.module.vcs_type == "git":
+ # tester "git checkout %(branch)s && git clean -dfq && git pull origin/%(branch)s"
+ command_list.append((modulepath, ['git', 'checkout', '-f', self.name]))
+ command_list.append((modulepath, ['git', 'fetch']))
+ command_list.append((modulepath, ['git', 'reset', '--hard', 'origin/%s' % self.name]))
+ command_list.append((modulepath, ['git', 'clean', '-dfq']))
+ # check if there are any submodules and init & update them
+ command_list.append((modulepath, "if [ -e .gitmodules ]; then git submodule update --init; fi"))
+ elif self.module.vcs_type == "bzr":
+ command_list.append((modulepath, ['bzr', 'up']))
+ if execute:
+ for working_dir, command in command_list:
+ utils.run_shell_command(command, raise_on_error=True, cwd=working_dir)
+ else:
+ return command_list
+
+ def commit_po(self, po_file, domain, language, author, sync_master=False):
""" Commit the file 'po_file' in the branch VCS repository """
if self.is_vcs_readonly():
raise Exception("This branch is in read-only mode. Unable to commit")
vcs_type = self.module.vcs_type
if vcs_type not in ("git",):
- raise Exception("Commit is not implemented for '%s'" % vcs_type)
+ raise NotImplementedError("Commit is not implemented for '%s'" % vcs_type)
locale = language.locale
commit_dir = os.path.join(self.co_path(), domain.directory)
@@ -655,13 +668,9 @@ class Branch(models.Model):
dest_filename = os.path.join(prefix, "%s.po" % locale)
dest_path = os.path.join(commit_dir, dest_filename)
- if vcs_type == "git":
- with ModuleLock(self.module):
- utils.run_shell_command(
- ['git', 'checkout', self.name], raise_on_error=True, cwd=commit_dir)
- utils.run_shell_command(
- ['git', 'pull'], raise_on_error=True, cwd=commit_dir)
-
+ with ModuleLock(self.module):
+ self.update_repo()
+ if vcs_type == "git":
already_exist = os.access(dest_path, os.F_OK)
if not already_exist and domain.dtype != 'ui':
raise Exception("Sorry, adding new translations for documentation is not yet supported.")
@@ -696,6 +705,11 @@ class Branch(models.Model):
utils.run_shell_command(
['git', 'reset', '--hard', 'origin/%s' % self.name], cwd=commit_dir)
raise
+ else:
+ _, out, _ = utils.run_shell_command(
+ ['git', 'log', '-n1', '--format=oneline'], cwd=commit_dir)
+ commit_hash = out.split()[0] if out else ''
+
# Finish by updating stats
if already_exist:
stat = Statistics.objects.get(language=language, branch=self, domain=domain)
@@ -703,6 +717,25 @@ class Branch(models.Model):
else:
self.update_stats(force=False, checkout=False, domain=domain)
+ if sync_master and not self.is_head():
+ # Cherry-pick the commit on the master branch
+ self.module.get_head_branch().cherrypick_commit(commit_hash, domain)
+
+ def cherrypick_commit(self, commit_hash, domain):
+ if self.module.vcs_type != "git":
+ raise NotImplementedError("Commit cherry-pick is not implemented for '%s'" %
self.module.vcs_type)
+ with ModuleLock(self.module):
+ self.update_repo()
+ commit_dir = os.path.join(self.co_path(), domain.directory)
+ result = utils.run_shell_command(
+ ['git', 'cherry-pick', '-x', commit_hash], cwd=commit_dir)
+ if result[0] == utils.STATUS_OK:
+ utils.run_shell_command(
+ ['git', 'push', 'origin', self.name], raise_on_error=True, cwd=commit_dir)
+ else:
+ # Revert
+ utils.run_shell_command(['git', 'cherry-pick', '--abort'], cwd=commit_dir)
+
DOMAIN_TYPE_CHOICES = (
('ui', 'User Interface'),
diff --git a/stats/tests/tests.py b/stats/tests/tests.py
index 5d7e73f..fb45b46 100644
--- a/stats/tests/tests.py
+++ b/stats/tests/tests.py
@@ -66,18 +66,29 @@ def mocked_checkout(branch):
class patch_shell_command:
"""
- Mock utils.run_shell_commands and gather all passed commands
+ Mock utils.run_shell_commands and gather all passed commands.
+ `only` is an optional list of commands to limit mocking to (empty -> all).
"""
+ def __init__(self, only=None):
+ self.only = only
+
def __enter__(self):
self.cmds = []
- def mocked_run_shell_command(cmd, *args, **kwargs):
- self.cmds.append(" ".join(cmd) if isinstance(cmd, list) else cmd)
- return 0, '', ''
self.saved_run_shell_command = utils.run_shell_command
- utils.run_shell_command = mocked_run_shell_command
+ utils.run_shell_command = self.mocked_run_shell_command
return self.cmds
- def __exit__(self, type, value, traceback):
+ def mocked_run_shell_command(self, cmd, *args, **kwargs):
+ cmd_str = " ".join(cmd) if isinstance(cmd, list) else cmd
+ self.cmds.append(cmd_str)
+ if self.only is not None and not any(needle in cmd_str for needle in self.only):
+ # Pass the command to the real utils.run_shell_command
+ return self.saved_run_shell_command(cmd, *args, **kwargs)
+ else:
+ # Pretend the command was successfull
+ return 0, '', ''
+
+ def __exit__(self, *args):
utils.run_shell_command = self.saved_run_shell_command
@@ -89,7 +100,7 @@ class ModuleTestCase(TestCase):
('gnome-doc-utils', 'xml2po'),
)
def __init__(self, name):
- TestCase.__init__(self, name)
+ super(ModuleTestCase, self).__init__(name)
for package, prog in self.SYS_DEPENDENCIES:
if not utils.check_program_presence(prog):
raise Exception("You are missing a required system package needed by Damned Lies (%s)" %
package)
@@ -292,11 +303,17 @@ class ModuleTestCase(TestCase):
self.mod.vcs_root = self.mod.vcs_root.replace('git://', 'ssh://')
self.mod.save()
+ update_repo_sequence = (
+ 'git checkout -f master', 'git fetch', 'git reset --hard origin/master',
+ 'git clean -dfq', 'if [ -e .gitmodules ]; then git submodule update --init; fi',
+ )
# User interface (existing language)
- git_ops = ('git checkout master', 'git pull', 'git add fr.po',
- # Quoting is done at the Popen level
- 'git commit -m Updated French translation --author Author <someone example org>',
- 'git push origin master')
+ git_ops = update_repo_sequence + (
+ 'git add fr.po',
+ # Quoting is done at the Popen level
+ 'git commit -m Updated French translation --author Author <someone example org>',
+ 'git push origin master'
+ )
with patch_shell_command() as cmds:
branch.commit_po(po_file, domain, fr_lang, 'Author <someone example org>')
for idx, cmd in enumerate(git_ops):
@@ -304,9 +321,11 @@ class ModuleTestCase(TestCase):
# User interface (new language)
bem_lang = Language.objects.get(locale='bem')
- git_ops = ('git checkout master', 'git pull', 'git add bem.po', 'git add LINGUAS',
- 'git commit -m Added Bemba translation --author Author <someone example org>',
- 'git push origin master')
+ git_ops = update_repo_sequence + (
+ 'git add bem.po', 'git add LINGUAS',
+ 'git commit -m Added Bemba translation --author Author <someone example org>',
+ 'git push origin master'
+ )
with patch_shell_command() as cmds:
branch.commit_po(po_file, domain, bem_lang, 'Author <someone example org>')
for idx, cmd in enumerate(git_ops):
@@ -319,15 +338,55 @@ class ModuleTestCase(TestCase):
# Documentation
domain = self.mod.domain_set.get(name='help')
- git_ops = ('git checkout master', 'git pull', 'git add fr/fr.po',
- 'git commit -m Updated French translation --author Author <someone example org>',
- 'git push origin master')
+ git_ops = update_repo_sequence + (
+ 'git add fr/fr.po',
+ 'git commit -m Updated French translation --author Author <someone example org>',
+ 'git push origin master'
+ )
with patch_shell_command() as cmds:
branch.commit_po(po_file, domain, fr_lang, 'Author <someone example org>')
for idx, cmd in enumerate(git_ops):
self.assertIn(cmd, cmds[idx])
@test_scratchdir
+ def test_commit_and_cherrypick(self):
+ """
+ Committing in non-HEAD branch and checking "sync with master" will
+ cherry-pick the branch commit on the master branch.
+ """
+ domain = self.mod.domain_set.get(name='po')
+ commit_dir = os.path.join(self.branch.co_path(), domain.directory)
+ utils.run_shell_command(['git', 'checkout', '-b', 'gnome-3-18', 'origin/master'], cwd=commit_dir)
+ branch = Branch.objects.create(module=self.mod, name='gnome-3-18')
+ fr_lang = Language.objects.get(locale='fr')
+ # Copy stats from master
+ stat = Statistics.objects.get(language=fr_lang, branch=self.branch, domain=domain)
+ stat.pk, stat.full_po, stat.part_po = None, None, None
+ stat.branch = branch
+ stat.save()
+ po_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test.po')
+ self.mod.vcs_root = self.mod.vcs_root.replace('git://', 'ssh://')
+ self.mod.save()
+
+ update_repo_sequence = (
+ 'git checkout -f gnome-3-18', 'git fetch', 'git reset --hard origin/gnome-3-18',
+ 'git clean -dfq', 'if [ -e .gitmodules ]; then git submodule update --init; fi',
+ )
+ update_repo_sequence_master = tuple(cmd.replace('gnome-3-18', 'master') for cmd in
update_repo_sequence)
+ git_ops = update_repo_sequence + (
+ 'git add fr.po',
+ 'git commit -m Updated French translation --author Author <someone example org>',
+ 'git push origin gnome-3-18', 'git log -n1 --format=oneline', 'msgfmt --statistics -o /dev/null',
+ ) + update_repo_sequence_master + (
+ 'git cherry-pick -x',
+ 'git push origin master',
+ )
+ with patch_shell_command(only=['git pull', 'git push', 'git fetch', 'git reset']) as cmds:
+ branch.commit_po(po_file, domain, fr_lang, 'Author <someone example org>', sync_master=True)
+ for idx, cmd in enumerate(git_ops):
+ self.assertIn(cmd, cmds[idx])
+
+ @test_scratchdir
def test_branch_file_changed(self):
# file_hashes is empty in fixture, so first call should always return True
self.assertTrue(self.mod.get_head_branch().file_changed("gnome-hello.doap"))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]