Git contrib

This commit is contained in:
Wendy Wang 2022-10-06 11:42:58 +02:00
parent 5d740ea98f
commit 1de1625aae
5 changed files with 313 additions and 0 deletions

View file

@ -0,0 +1,64 @@
# In-game Git Integration
Contribution by helpme (2022)
A module to integrate a stripped-down version of git within the game, allowing developers to keep their evennia version updated, commit code to their git repository, change branches, and pull updated code. After a successful pull or checkout, the git command will reload the game: You may need to restart manually to apply certain changes that would impact persistent scripts etc.
Once the contrib is set up, integrating remote changes is as simple as entering the following into your game:
```
git pull
```
Of course, your game directory must be a git directory to begin with for this command to function.
## Dependencies
This package requires the dependency "gitpython", a python library used to interact with git repositories. To install, it's easiest to install Evennia's extra requirements:
- Activate your `virtualenv`
- `cd` to the root of the Evennia repository. There should be an `requirements_extra.txt` file here.
- `pip install -r requirements_extra.txt`
## Installation
This utility adds a simple assortment of 'git' commands. Import the module into your commands and add it to your command set to make it available.
Specifically, in `mygame/commands/default_cmdsets.py`:
```python
...
from evennia.contrib.utils.git_integration import GitCmdSet # <---
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(GitCmdSet) # <---
```
Then `reload` to make the git command available.
## Usage
This utility will only work if your game and evennia directories are git directories. If they are not, you will be prompted to initiate your directory as a git repository using the following commands in your terminal:
```
git init
git remote add origin [link to your repository]
```
By default, the git commands are only available to those with Developer permissions and higher. You can change this by overriding the command and setting its locks from "cmd:pperm(Developer)" to the lock of your choice.
The supported commands are:
* git status: An overview of your git repository, which files have been changed locally, and the commit you're on.
* git status evennia: An overview of your evennia repository and the commit you're on.
* git branch: What branches are available for you to check out.
* git checkout: Checkout a branch.
* git pull: Pull the latest code from your current branch.
* git pull evennia: Pull the latest evennia code.
## Settings Used
The utility uses the existing GAME_DIR and EVENNIA_DIR settings from settings.py. You should not need to alter these if you have a standard directory setup.

View file

@ -0,0 +1,5 @@
"""
Git integration - helpme 2022
"""
from .git_integration import GitCmdSet # noqa

View file

@ -0,0 +1,166 @@
from django.conf import settings
from evennia import CmdSet, InterruptCommand
from evennia.utils.utils import list_to_string
from evennia.commands.default.muxcommand import MuxCommand
from evennia.server.sessionhandler import SESSIONS
import git
import datetime
class GitCommand(MuxCommand):
"""
Parent class for Git commands.
"""
def parse(self):
"""
Parse the arguments and ensure git repositories exist. Fail with InterruptCommand if git repositories not found.
"""
if self.args:
split_args = self.args.strip().split(" ", 1)
self.action = split_args[0]
if len(split_args) > 1:
self.args = ''.join(split_args[1:])
else:
self.args = ''
else:
self.action = "status"
self.args = ""
err_msgs = ["|rInvalid Git Repository|n:",
"The {repo_type} repository is not recognized as a git directory.",
"In order to initialize it as a git directory, you will need to access your terminal and run the following commands from within your directory:",
" git init",
" git remote add origin {remote_link}"]
if self.cmdstring == "git evennia":
directory = settings.EVENNIA_DIR
repo_type = "Evennia"
remote_link = "https://github.com/evennia/evennia.git"
else:
directory = settings.GAME_DIR
repo_type = "game"
remote_link = "[your remote link]"
try:
self.repo = git.Repo(directory, search_parent_directories=True)
except git.exc.InvalidGitRepositoryError:
err_msg = '\n'.join(err_msgs).format(repo_type=repo_type, remote_link=remote_link)
self.caller.msg(err_msg)
raise InterruptCommand
self.commit = self.repo.head.commit
self.branch = self.repo.active_branch.name
def short_sha(self, repo, hexsha):
"""
Utility: Get the short SHA of a commit.
"""
short_sha = repo.git.rev_parse(hexsha, short=True)
return short_sha
class CmdGit(GitCommand):
"""
Pull the latest code from your repository or checkout a different branch.
Usage:
git status - View an overview of your git repository.
git branch - View available branches.
git checkout main - Checkout the main branch of your code.
git pull - Pull the latest code from your current branch.
For updating evennia code, the same commands are available with 'git evennia':
git evennia status
git evennia branch
git evennia checkout <branch>
git evennia pull
If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for changes involving persistent scripts etc, you may need to manually restart.
"""
key = "@git"
aliases = ["@git evennia"]
locks = "cmd:pperm(Developer)"
help_category = "System"
def get_status(self):
time_of_commit = datetime.datetime.fromtimestamp(self.commit.committed_date)
status_msg = '\n'.join([f"Branch: |w{self.branch}|n ({self.repo.git.rev_parse(self.commit.hexsha, short=True)}) ({time_of_commit})",
f"By {self.commit.author.email}: {self.commit.message}"])
changedFiles = { item.a_path for item in self.repo.index.diff(None) }
if changedFiles:
status_msg += f"Unstaged/uncommitted changes:|/ |g{'|/ '.join(changedFiles)}|n|/"
if len(self.repo.untracked_files) > 0:
status_msg += f"Untracked files:|/ |x{'|/ '.join(self.repo.untracked_files)}|n"
return status_msg
def get_branches(self):
remote_refs = self.repo.remote().refs
branch_msg = f"Current branch: |w{self.branch}|n. Branches available: {list_to_string(remote_refs)}"
return branch_msg
def checkout(self):
remote_refs = self.repo.remote().refs
to_branch = self.args.strip().removeprefix('origin/') # Slightly hacky, but git tacks on the origin/
if to_branch not in remote_refs:
self.caller.msg(f"Branch '{to_branch}' not available.")
return False
elif to_branch == self.branch:
self.caller.msg(f"Already on |w{to_branch}|n. Maybe you want <git pull>?")
return False
else:
try:
self.repo.git.checkout(to_branch)
except git.exc.GitCommandError as err:
self.caller.msg("Couldn't checkout {} ({})".format(to_branch, err.stderr.strip()))
return False
self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.")
return True
def pull(self):
old_commit = self.commit
try:
self.repo.remotes.origin.pull()
except git.exc.GitCommandError as err:
self.caller.msg("Couldn't pull {} ({})".format(self.branch, err.stderr.strip()))
return False
if old_commit == self.repo.head.commit:
self.caller.msg("No new code to pull, no need to reset.\n")
return False
else:
self.caller.msg(f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}")
return True
def func(self):
"""
Provide basic Git functionality within the game.
"""
caller = self.caller
if self.action == "status":
caller.msg(self.get_status())
elif self.action == "branch" or (self.action == "checkout" and not self.args):
caller.msg(self.get_branches())
elif self.action == "checkout":
if self.checkout():
SESSIONS.portal_restart_server()
elif self.action == "pull":
if self.pull():
SESSIONS.portal_restart_server()
else:
caller.msg("You can only git status, git branch, git checkout, or git pull.")
return
# CmdSet for easily install all commands
class GitCmdSet(CmdSet):
"""
The map command.
"""
def at_cmdset_creation(self):
self.add(CmdGit)

View file

@ -0,0 +1,75 @@
"""
Tests of git.
"""
from evennia import InterruptCommand
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import EvenniaTest
from evennia.contrib.utils.git.git_integration import CmdGit
from evennia.utils.utils import list_to_string
import git
import mock
import datetime
class TestCmdGit(CmdGit):
pass
class TestGit(EvenniaTest):
@mock.patch("git.Repo")
@mock.patch("git.Git")
@mock.patch("git.Actor")
def setUp(self, mock_git, mock_repo, mock_author):
super().setUp()
self.char1.msg = mock.Mock()
p = mock_git.return_value = False
type(mock_repo.clone_from.return_value).bare = p
mock_repo.index.add(["mock.txt"])
mock_git.Repo.side_effect = git.exc.InvalidGitRepositoryError
mock_author.name = "Faux Author"
mock_author.email = "a@email.com"
commit_date = datetime.datetime(2021, 2, 1)
mock_repo.index.commit(
"Initial skeleton",
author=mock_author,
committer=mock_author,
author_date=commit_date,
commit_date=commit_date,
)
test_cmd_git = TestCmdGit()
test_cmd_git.repo = mock_repo
test_cmd_git.commit = mock_git.head.commit
test_cmd_git.branch = mock_git.active_branch.name
test_cmd_git.caller = self.char1
test_cmd_git.args = "nonexistent_branch"
self.test_cmd_git = test_cmd_git
def test_git_status(self):
time_of_commit = datetime.datetime.fromtimestamp(self.test_cmd_git.commit.committed_date)
status_msg = '\n'.join([f"Branch: |w{self.test_cmd_git.branch}|n ({self.test_cmd_git.repo.git.rev_parse(self.test_cmd_git.commit.hexsha, short=True)}) ({time_of_commit})",
f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}"])
self.assertEqual(status_msg, self.test_cmd_git.get_status())
def test_git_branch(self):
# View current branch
remote_refs = self.test_cmd_git.repo.remote().refs
branch_msg = f"Current branch: |w{self.test_cmd_git.branch}|n. Branches available: {list_to_string(remote_refs)}"
self.assertEqual(branch_msg, self.test_cmd_git.get_branches())
def test_git_checkout(self):
# Checkout no branch
self.test_cmd_git.checkout()
self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.")
def test_git_pull(self):
self.test_cmd_git.pull()
repo = self.test_cmd_git.repo
self.char1.msg.assert_called_with(f"You have pulled new code. Server restart initiated.|/Head now at {repo.git.rev_parse(repo.head.commit.hexsha, short=True)}.|/Author: {repo.head.commit.author.name} ({repo.head.commit.author.email})|/{repo.head.commit.message.strip()}")

View file

@ -22,3 +22,6 @@ django-extensions >= 3.1.0
# xyzroom contrib
scipy<1.9
# Git contrib
gitpython >= 3.1.27