From 8492d42d125f8a5e22ef00a01eaebaa2378141e0 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Sep 2025 11:22:20 +0200 Subject: [PATCH 1/6] Fix loading _manifest in a project git --- osc/git_scm/store.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osc/git_scm/store.py b/osc/git_scm/store.py index 402d00a5..a3af1083 100644 --- a/osc/git_scm/store.py +++ b/osc/git_scm/store.py @@ -135,22 +135,23 @@ class LocalGitStore: break if self.type == "project": + manifest_path = os.path.join(self._git.topdir, "_manifest") + subdirs_path = os.path.join(self._git.topdir, "_subdirs") + + if os.path.exists(manifest_path): + self.manifest = Manifest.from_file(manifest_path) + elif os.path.exists(subdirs_path): + self.manifest = Subdirs.from_file(subdirs_path) + else: + # empty manifest considers all top-level directories as packages + self.manifest = Manifest({}) + if self._git.topdir != self.abspath: - manifest_path = os.path.join(self._git.topdir, "_manifest") - subdirs_path = os.path.join(self._git.topdir, "_subdirs") - - if os.path.exists(manifest_path): - self.manifest = Manifest.from_file(manifest_path) - elif os.path.exists(subdirs_path): - self.manifest = Subdirs.from_file(subdirs_path) - else: - # empty manifest considers all top-level directories as packages - self.manifest = Manifest() - package_topdir = self.manifest.resolve_package_path(project_path=self._git.topdir, package_path=self.abspath) if package_topdir: self._type = "package" self._topdir = package_topdir + self.manifest = None self.project_store = None if self.type == "package": From 14d589694ab13abc28ec9a12077370441841f22c Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Sep 2025 11:44:44 +0200 Subject: [PATCH 2/6] Be more permissive when loading parent project_store in GitStore --- osc/git_scm/store.py | 15 ++++++++++++--- tests/test_git_scm_store.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/osc/git_scm/store.py b/osc/git_scm/store.py index a3af1083..c7872b55 100644 --- a/osc/git_scm/store.py +++ b/osc/git_scm/store.py @@ -156,14 +156,23 @@ class LocalGitStore: self.project_store = None if self.type == "package": # load either .osc or .git project store from the directory above topdir - for cls in (Store, self.__class__): + if not self.project_store: try: - store = cls(os.path.join(self.topdir, "..")) + store = Store(os.path.join(self.topdir, "..")) store.assert_is_project() self.project_store = store - break except oscerr.NoWorkingCopy: pass + + if not self.project_store: + try: + # turn off 'check' because we want at least partial metadata to be inherited to the package + store = GitStore(os.path.join(self.topdir, ".."), check=False) + if store.type == "project": + self.project_store = store + except oscerr.NoWorkingCopy: + pass + elif self.type == "project": # load .osc project store that is next to .git and may provide medatata we don't have try: diff --git a/tests/test_git_scm_store.py b/tests/test_git_scm_store.py index cde74efb..a33334e6 100644 --- a/tests/test_git_scm_store.py +++ b/tests/test_git_scm_store.py @@ -103,6 +103,9 @@ class TestGitStoreProject(unittest.TestCase): if separate_git_dir: git_init_cmd += ["--separate-git-dir", separate_git_dir] subprocess.check_output(git_init_cmd, cwd=path) + subprocess.check_output(["git", "config", "user.email", "user@example.com"], cwd=path) + subprocess.check_output(["git", "config", "user.name", "User Name"], cwd=path) + subprocess.check_output(["git", "commit", "-m", "empty", "--allow-empty"], cwd=path) subprocess.check_output(["git", "checkout", "-b", "factory", "-q"], cwd=path) subprocess.check_output(["git", "remote", "add", "origin", "https://example.com/packages/my-package.git"], cwd=path) @@ -326,6 +329,27 @@ class TestGitStoreProject(unittest.TestCase): with self.assertRaises(oscerr.NoWorkingCopy): GitStore(pkg_path) + def test_pkg_git_in_submodule(self): + import subprocess + + pkg_upstream_path = os.path.join(self.tmpdir, "pkg-upstream") + self._git_init(pkg_upstream_path) + + prj_path = os.path.join(self.tmpdir, "project") + self._git_init(prj_path) + manifest_data = { + "obs_apiurl": "https://api.example.com", + "obs_project": "PROJ", + } + self._write(os.path.join(prj_path, "_manifest"), osc_yaml.yaml_dumps(manifest_data)) + + subprocess.check_output(["git", "-c", "protocol.file.allow=always", "submodule", "add", pkg_upstream_path, "pkg"], cwd=prj_path, stderr=subprocess.DEVNULL) + pkg_path = os.path.join(prj_path, "pkg") + + store = GitStore(pkg_path) + self.assertEqual(store.project, "PROJ") + self.assertEqual(store.package, "pkg-upstream") + if __name__ == "__main__": unittest.main() From d0a6aef1ddf5a710cd8f293b3b32cb1d9813a39a Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Sep 2025 16:40:29 +0200 Subject: [PATCH 3/6] Improve GitStore's error messages by adding instructions on how to fix missing metadata --- osc/git_scm/store.py | 68 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/osc/git_scm/store.py b/osc/git_scm/store.py index c7872b55..fde19c91 100644 --- a/osc/git_scm/store.py +++ b/osc/git_scm/store.py @@ -255,11 +255,28 @@ class LocalGitStore: for name in ["apiurl", "project"]: if not getattr(self, name): missing.append(name) + if missing: - msg = ( - f"Git SCM project working copy doesn't have the following metadata set: {', '.join(missing)}\n" - "Use 'git-obs meta pull' or 'git-obs meta set' to fix that" - ) + msg = f"Git SCM project working copy doesn't have the following metadata set: {', '.join(missing)}\n" + + if "apiurl" in missing: + msg += ( + "\n" + "To fix apiurl:\n" + " - Run 'git-obs meta pull' to retrieve the 'obs_apiurl' value from 'obs/configuration' repo, 'main' branch, 'configuration.yaml' file\n" + " - Run 'git-obs meta set --apiurl=...\n" + ) + + if "project" in missing: + msg += ( + "\n" + "To fix project:\n" + " - Set 'obs_project' in '_manifest' file\n" + " - Run 'git-obs meta set --project=...\n" + ) + + msg += "\nCheck git-obs-metadata man page for more details" + raise oscerr.NoWorkingCopy(msg) def assert_is_package(self): @@ -271,11 +288,46 @@ class LocalGitStore: for name in ["apiurl", "project", "package"]: if not getattr(self, name): missing.append(name) + if missing: - msg = ( - f"Git SCM package working copy doesn't have the following metadata set: {', '.join(missing)}\n" - "Use 'git-obs meta pull' or 'git-obs meta set' to fix that" - ) + msg = f"Git SCM package working copy doesn't have the following metadata set: {', '.join(missing)}\n" + + if self.project_store: + msg += f" - The package has a parent project checkout: {self.project_store.abspath}\n" + else: + msg += " - The package has no parent project checkout\n" + + if "apiurl" in missing: + msg += "\n" + msg += "To fix apiurl:\n" + if self.project_store: + msg += ( + " - Run 'git-obs meta pull' IN THE PROJECT in the parent directory to retrieve the 'obs_apiurl' value from 'obs/configuration' repo, 'main' branch, 'configuration.yaml' file\n" + " - run 'git-obs meta set --apiurl=...' IN THE PROJECT\n" + ) + else: + msg += ( + " - Run 'git-obs meta set --apiurl=...'\n" + ) + + if "project" in missing: + msg += "\n" + msg += "To fix project:\n" + + if self.project_store: + msg += ( + " - Set 'obs_project' in '_manifest' file IN THE PROJECT\n" + " - Run 'git-obs meta set --project=...' IN THE PROJECT\n" + ) + else: + msg += ( + f" - Set 'obs_project' in the matching _ObsPrj git repo, '{self._git.current_branch}' branch, '_manifest' file\n" + " Run 'git-obs meta pull'\n" + " - Run 'git-obs meta set --project=...'\n" + ) + + msg += "\nCheck git-obs-metadata man page for more details" + raise oscerr.NoWorkingCopy(msg) # APIURL From 86ab676f57cd85aaca17ed5d50c73b08ac67e34a Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Sep 2025 16:41:36 +0200 Subject: [PATCH 4/6] spec: Install git-obs-metadata man page --- contrib/osc.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/osc.spec b/contrib/osc.spec index 0e4d2d79..96c0e1dd 100644 --- a/contrib/osc.spec +++ b/contrib/osc.spec @@ -239,6 +239,7 @@ install -Dm0644 macros.osc %{buildroot}%{_rpmmacrodir}/macros.osc # install man page %if %{with man} +install -Dm0644 git-obs-metadata.1 %{buildroot}%{_mandir}/man1/git-obs-metadata.1 install -Dm0644 git-obs-quickstart.1 %{buildroot}%{_mandir}/man1/git-obs-quickstart.1 install -Dm0644 osc.1 %{buildroot}%{_mandir}/man1/osc.1 install -Dm0644 git-obs.1 %{buildroot}%{_mandir}/man1/git-obs.1 From a241041db5ed14b4d7bb820ca3b46793429658f9 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 24 Sep 2025 16:46:44 +0200 Subject: [PATCH 5/6] docs: Add git-obs-metadata to the documentation index --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/index.rst b/doc/index.rst index d44177aa..c16c72be 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,6 +20,7 @@ API: :maxdepth: 2 git-obs-quickstart + git-obs-metadata api/modules plugins/index oscrc From bb7987a1a55b08f5bb67661a4fd3f317a3743fd9 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Thu, 25 Sep 2025 10:19:01 +0200 Subject: [PATCH 6/6] Rephrase the error message about detached HEAD in GitStore --- osc/git_scm/store.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osc/git_scm/store.py b/osc/git_scm/store.py index fde19c91..3ac87a3d 100644 --- a/osc/git_scm/store.py +++ b/osc/git_scm/store.py @@ -117,7 +117,10 @@ class LocalGitStore: if not self._git.current_branch: # branch is required for determining and storing metadata - msg = f"Directory '{path}' is not a Git SCM working copy because it has no branch or is in a detached HEAD state" + msg = ( + f"Directory '{path}' contains a git repo that has no branch or is in a detached HEAD state.\n" + "If it is a Git SCM working copy, switch to a branch to continue." + ) raise oscerr.NoWorkingCopy(msg) # 'package' is the default type that applies to all git repos that are not projects (we have no means of detecting packages)