From c91e2899d3553bfc0d6e8404c3c47e21d5553238 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Fri, 23 Feb 2024 13:51:28 +0100 Subject: [PATCH] git: add `Remote` component to add/change the git remote of a checkout FC-36013 While restructuring a Wordpress monorepo deployment, we had the issue that * batou cloned from `/srv/s-service/deployment` to `/mnt/nfs/shared/project` * we deployed the new structure that did a `git clone` of Wordpress from a GitLab rather than a local path * batou noticed that the remote URL was different and threw the old checkout away before cloning the new one rather than pulling. Normally, this wouldn't be an issue, but wordpress stores state inside the checkout, e.g. in `/wp-content/uploads` and this was gone. Not a big deal when you have backups, but an automatic migration path is nicer. This patch introduces a very basic building block for that, a component that ensures that a given git checkout has a given remote with a given name & url. I.e. self += git.Remote("/path/to/repo", url="git@github.com:nixos/nixpkgs") ensures that the repository under `/path/to/repo` has a remote called `origin` that points to `git@github.com:nixos/nixpkgs`. This fails if the directory doesn't exist or if it isn't a git repository. For cloning, you still need to use `Clone` from batou or `GitCheckout` from `batou_ext`. However, in the case described above, this shouldn't fail if the checkout doesn't exist at all: in that case there's no existing repository to "migrate", but it's a fresh deployment that should run through. For that, the component can be skipped if no repo exists at all like this: self += git.Remote( "/path/to/repo", url="git@github.com:nixos/nixpkgs", ignore_not_existing=True ) --- .../20240404_114123_git_remote_component.md | 1 + src/batou_ext/git.py | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 CHANGES.d/20240404_114123_git_remote_component.md diff --git a/CHANGES.d/20240404_114123_git_remote_component.md b/CHANGES.d/20240404_114123_git_remote_component.md new file mode 100644 index 0000000..6e33c45 --- /dev/null +++ b/CHANGES.d/20240404_114123_git_remote_component.md @@ -0,0 +1 @@ +* Added a component `batou_ext.git.Remote` which allows to manipulate remotes of a git repository. diff --git a/src/batou_ext/git.py b/src/batou_ext/git.py index 9fcee07..3133d74 100644 --- a/src/batou_ext/git.py +++ b/src/batou_ext/git.py @@ -144,6 +144,68 @@ def has_changes(self): return bool(stdout.strip()) +class Remote(batou.component.Component): + """ + Ensure that a git repository checkout out at `git_repo` has the origin + `name` pointing to `url`. + + Usage:: + + self += batou_ext.git.Remote( + "/path/to/linux", + name="upstream", + url="ssh://git@github.com/torvalds/linux" + ) + + By default, the component aborts with an error if `git_repo` does not + exist or is not a git repository. This is not always desirable, for + instance when this is involved in migrating an existing `git_repo` + to a new remote: then, this works fine for migration, but not when + bootstrapping e.g. a new environment where `git_repo` doesn't exist + yet. + + To solve that, it's possible to let the component do nothing if + `git_repo` doesn't exist like this: + + self += batou_ext.git.Remote( + "/path/to/linux", + ignore_not_existing=True, + # ... + ) + """ + + namevar = "git_repo" + url = batou.component.Attribute(str) + name = batou.component.Attribute(str, "origin") + ignore_not_existing = batou.component.Attribute(bool, False) + + def verify(self): + if not os.path.exists(self.git_repo): + if self.ignore_not_existing: + return + else: + raise ValueError( + f"batou_ext.git.Remote({self.git_repo}): path does not exist!" + ) + elif not os.path.exists(f"{self.git_repo}/.git"): + raise ValueError( + f"batou_ext.git.Remote({self.git_repo}): not a git repository!" + ) + + with self.chdir(self.git_repo): + stdout, _ = self.cmd( + f"git remote get-url {self.name}", ignore_returncode=True + ) + if self.url != stdout.strip(): + raise batou.UpdateNeeded() + + def update(self): + with self.chdir(self.git_repo): + stdout, _ = self.cmd("git remote") + verb = "set-url" if self.name in stdout.splitlines() else "add" + self.cmd(f"git remote {verb} {self.name} {self.url}") + + class Push(batou.component.Component): """`git push` if there are outgoing changes."""