From 69095b74919045e49922b9be8b8016ff2d95a80f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Sat, 3 Apr 2021 06:29:23 +0200
Subject: [PATCH] grid: note git revision when installing from a git checkout

Installing from a git checkout is a bad practice for production, but it's a
common thing during development. One problem I often face is that I have a
software release installed from a given revision and I want to make a small
change to the software, but not recompile everything. To achieve this, I need
to use the exact same git revision that was installed before, but most of the
time, I don't remember what revision I have been using last time I installed.

This change is about adding a buildout comment in the generated buildout.cfg
made to install the profile, with the revision that was used for installing, so
that we can re-install the same software again.
---
 slapos/grid/SlapObject.py   | 28 ++++++++++++++++++++++++++++
 slapos/tests/test_object.py | 31 +++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+)

diff --git a/slapos/grid/SlapObject.py b/slapos/grid/SlapObject.py
index b6463080e..580d927d2 100644
--- a/slapos/grid/SlapObject.py
+++ b/slapos/grid/SlapObject.py
@@ -28,6 +28,7 @@
 #
 ##############################################################################
 
+import datetime
 import errno
 import os
 import pkg_resources
@@ -284,6 +285,7 @@ class Software(object):
       buildout_cfg = os.path.join(self.software_path, 'buildout.cfg')
       if not os.path.exists(buildout_cfg):
         self._create_buildout_profile(buildout_cfg, self.url, self.shared_part_list)
+      self._note_git_revision(buildout_cfg, self.url)
 
       additional_parameters = list(self._additional_buildout_parameters(extends_cache))
       additional_parameters.extend(['-c', buildout_cfg])
@@ -327,6 +329,32 @@ class Software(object):
       parser.write(fout)
     self._set_ownership(buildout_cfg)
 
+  def _note_git_revision(self, buildout_cfg, url):
+    """Add a comment with the revision if the profile is a git checkout.
+
+    This makes it easier to rebuild the same revision.
+    """
+    git_revision = None
+    if os.path.exists(url):
+      try:
+        git_revision = subprocess.check_output(
+            ('git', 'describe', '--all', '--long', '--dirty'),
+            cwd=os.path.dirname(url),
+        ).decode()
+      except (OSError, subprocess.CalledProcessError):
+        self.logger.debug(
+            'Error guessing git revision for profile %s',
+            url,
+            exc_info=True)
+      if git_revision:
+        with open(buildout_cfg) as f:
+          if git_revision in f.readlines()[-1]:
+            self.logger.debug('Current revision was already noted')
+            return
+        with open(buildout_cfg, 'a') as f:
+          f.write(
+              "# %s git revision: %s" % (datetime.datetime.utcnow(), git_revision))
+
   def uploadSoftwareRelease(self, tarpath):
     """
     Try to tar and upload an installed Software Release.
diff --git a/slapos/tests/test_object.py b/slapos/tests/test_object.py
index 456bbc1ff..3d6cb32a8 100644
--- a/slapos/tests/test_object.py
+++ b/slapos/tests/test_object.py
@@ -27,8 +27,11 @@
 
 import logging
 import os
+import subprocess
 import time
 import unittest
+import shutil
+import tempfile
 
 from slapos.slap import ComputerPartition as SlapComputerPartition
 
@@ -333,6 +336,34 @@ class TestSoftwareNetworkCacheSlapObject(MasterMixin, unittest.TestCase):
     software.install()
     self.assertTrue(getattr(self, 'uploaded', False))
 
+  def test_software_install_note_git_revision(self):
+    tmpdir = tempfile.mkdtemp()
+    self.addCleanup(shutil.rmtree, tmpdir)
+
+    subprocess.check_call(('git', 'init'), cwd=tmpdir)
+    profile_url = os.path.join(tmpdir, 'software.cfg')
+    open(profile_url, 'w').close()
+    subprocess.check_call(('git', 'add', 'software.cfg'), cwd=tmpdir)
+    subprocess.check_call(
+        ('git', 'commit', '-m', 'first commit'),
+        cwd=tmpdir,
+        env=dict(
+            os.environ,
+            GIT_AUTHOR_NAME='nobody',
+            GIT_AUTHOR_EMAIL='nobody@example.com',
+            GIT_COMMITTER_NAME='nobody',
+            GIT_COMMITTER_EMAIL='nobody@example.com',))
+
+    software = Software(
+        url=profile_url,
+        software_root=self.software_root,
+        buildout=self.buildout,
+        logger=logging.getLogger())
+    software.install()
+    with open(os.path.join(software.software_path, 'buildout.cfg')) as f:
+      self.assertIn("git revision: heads/master-0-g", f.read())
+
+
 class TestPartitionSlapObject(MasterMixin, unittest.TestCase):
   def setUp(self):
     MasterMixin.setUp(self)
-- 
2.30.9