mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge pull request #2907 from Machine-Garden-MUD/develop
Git Contrib: Integrating Git status, branch, checkout, and pull functionality into Evennia.
This commit is contained in:
commit
bab8071503
5 changed files with 342 additions and 0 deletions
68
evennia/contrib/utils/git_integration/README.md
Normal file
68
evennia/contrib/utils/git_integration/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# In-game Git Integration
|
||||
|
||||
Contribution by helpme (2022)
|
||||
|
||||
A module to integrate a stripped-down version of git within the game, allowing developers to view their git status, change branches, and pull updated code of both their local mygame repo and Evennia core. After a successful pull or checkout, the git command will reload the game: Manual restarts may be required to 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
|
||||
```
|
||||
|
||||
The repositories you want to work with, be it only your local mygame repo, only Evennia core, or both, must be git directories for the command to function. If you are only interested in using this to get upstream Evennia changes, only the Evennia repository needs to be a git repository. [Get started with version control here.](https://www.evennia.com/docs/1.0-dev/Coding/Version-Control.html)
|
||||
|
||||
## 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 the directory you wish to work with is a git directory. 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 branch: What branches are available for you to check out.
|
||||
* git checkout 'branch': Checkout a branch.
|
||||
* git pull: Pull the latest code from your current branch.
|
||||
|
||||
* All of these commands are also available with 'evennia', to serve the same functionality related to your Evennia directory. So:
|
||||
* git evennia status
|
||||
* git evennia branch
|
||||
* git evennia checkout 'branch'
|
||||
* git evennia pull: 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, they ought to exist without any setup required from you.
|
||||
5
evennia/contrib/utils/git_integration/__init__.py
Normal file
5
evennia/contrib/utils/git_integration/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
Git integration - helpme 2022
|
||||
"""
|
||||
|
||||
from .git_integration import GitCmdSet # noqa
|
||||
197
evennia/contrib/utils/git_integration/git_integration.py
Normal file
197
evennia/contrib/utils/git_integration/git_integration.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
from resource import error
|
||||
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):
|
||||
"""
|
||||
The shared functionality between git/git evennia
|
||||
"""
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Parse the arguments, set default arg to 'status' and check for existence of currently targeted repo
|
||||
"""
|
||||
super().parse()
|
||||
|
||||
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 = ""
|
||||
|
||||
self.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}"]
|
||||
|
||||
try:
|
||||
self.repo = git.Repo(self.directory, search_parent_directories=True)
|
||||
except git.exc.InvalidGitRepositoryError:
|
||||
err_msg = '\n'.join(self.err_msgs).format(repo_type=self.repo_type, remote_link=self.remote_link)
|
||||
self.caller.msg(err_msg)
|
||||
raise InterruptCommand
|
||||
|
||||
self.commit = self.repo.head.commit
|
||||
|
||||
try:
|
||||
self.branch = self.repo.active_branch.name
|
||||
except TypeError as type_err:
|
||||
self.caller.msg(type_err)
|
||||
raise InterruptCommand
|
||||
|
||||
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
|
||||
|
||||
def get_status(self):
|
||||
"""
|
||||
Retrieves the status of the active git repository, displaying unstaged changes/untracked files.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Display current and available branches.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Check out a specific branch.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Attempt to pull new code.
|
||||
"""
|
||||
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
|
||||
|
||||
class CmdGitEvennia(GitCommand):
|
||||
"""
|
||||
Pull the latest code from the evennia core or checkout a different branch.
|
||||
|
||||
Usage:
|
||||
git evennia status - View an overview of the evennia repository status.
|
||||
git evennia branch - View available branches in evennia.
|
||||
git evennia checkout <branch> - Checkout a different branch in evennia.
|
||||
git evennia pull - Pull the latest evennia code.
|
||||
|
||||
For updating your local mygame repository, the same commands are available with 'git'.
|
||||
|
||||
If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for some changes involving persistent scripts etc, you may need to manually restart.
|
||||
"""
|
||||
|
||||
key = "git evennia"
|
||||
locks = "cmd:pperm(Developer)"
|
||||
help_category = "System"
|
||||
directory = settings.EVENNIA_DIR
|
||||
repo_type = "Evennia"
|
||||
remote_link = "https://github.com/evennia/evennia.git"
|
||||
|
||||
|
||||
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'.
|
||||
|
||||
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"
|
||||
locks = "cmd:pperm(Developer)"
|
||||
help_category = "System"
|
||||
directory = settings.GAME_DIR
|
||||
repo_type = "game"
|
||||
remote_link = "[your remote link]"
|
||||
|
||||
|
||||
# CmdSet for easily install all commands
|
||||
class GitCmdSet(CmdSet):
|
||||
"""
|
||||
The git command.
|
||||
"""
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdGit)
|
||||
self.add(CmdGitEvennia)
|
||||
69
evennia/contrib/utils/git_integration/tests.py
Normal file
69
evennia/contrib/utils/git_integration/tests.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Tests of git.
|
||||
|
||||
"""
|
||||
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.contrib.utils.git_integration.git_integration import GitCommand
|
||||
from evennia.utils.utils import list_to_string
|
||||
|
||||
import git
|
||||
import mock
|
||||
import datetime
|
||||
|
||||
class TestGitIntegration(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 = GitCommand()
|
||||
self.repo = test_cmd_git.repo = mock_repo
|
||||
self.commit = test_cmd_git.commit = mock_git.head.commit
|
||||
self.branch = 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()
|
||||
self.char1.msg.assert_called_with(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()}")
|
||||
|
||||
|
|
@ -22,3 +22,6 @@ django-extensions >= 3.1.0
|
|||
|
||||
# xyzroom contrib
|
||||
scipy<1.9
|
||||
|
||||
# Git contrib
|
||||
gitpython >= 3.1.27
|
||||
Loading…
Add table
Add a link
Reference in a new issue