Page MenuHomeDevCentral

D2569.id6490.diff
No OneTemporary

D2569.id6490.diff

diff --git a/.arcconfig b/.arcconfig
new file mode 100644
--- /dev/null
+++ b/.arcconfig
@@ -0,0 +1,4 @@
+{
+ "phabricator.uri": "https://devcentral.nasqueron.org/",
+ "repository.callsign": "RESOLVEHASH"
+}
diff --git a/.arclint b/.arclint
new file mode 100644
--- /dev/null
+++ b/.arclint
@@ -0,0 +1,29 @@
+{
+ "linters": {
+ "chmod": {
+ "type": "chmod"
+ },
+ "filename": {
+ "type": "filename"
+ },
+ "json": {
+ "type": "json",
+ "include": [
+ "(^\\.arcconfig$)",
+ "(^\\.arclint$)",
+ "(\\.json$)"
+ ]
+ },
+ "python": {
+ "type": "flake8",
+ "severity": {
+ "E203": "disabled",
+ "F821": "advice"
+ },
+ "include": [
+ "(^bin/resolve-hash$)",
+ "(\\.py$)"
+ ]
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Python package build
+dist/
+*.egg-info/
+
+# Python
+__pycache__/
+*.pyc
+*.pyo
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright 2022 Sébastien Santoro aka Dereckson, from Nasqueron.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+RMDIR=rm -rf
+PYTHON=python
+DISCOVER_TESTS=$(PYTHON) -m unittest discover
+REFORMAT=black
+
+# -------------------------------------------------------------
+# Main targets
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+all:
+
+package: dist
+
+clean: clean-package
+
+test:
+ ${DISCOVER_TESTS} tests/
+
+# -------------------------------------------------------------
+# Development helpers
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+reformat:
+ find bin -type f -name '*.py' | xargs ${REFORMAT}
+ find src -type f -name '*.py' | xargs ${REFORMAT}
+ find tests -type f -name '*.py' | xargs ${REFORMAT}
+
+# -------------------------------------------------------------
+# Packaging targets
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+dist:
+ ${PYTHON} -m build
+
+clean-package:
+ ${RMDIR} dist src/resolve_hash.egg-info
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+## resolve-hash
+
+Allow to resolve a hash to a known URL representation.
+
+### Usage
+
+`resolve-hash <hash>` outputs URL matching the hash
+
+**Output:** URL matching the hash, if found
+
+**Exit code:**
+ * 0 if the hash has been found
+ * 1 if the hash has NOT been found
+
+**Example:**
+
+```shell
+$ resolve-hash 8d8645468228
+https://devcentral.nasqueron.org/rKERUALD8d8645468228
+
+$ resolve-hash 00000000000000 (git)-[main]
+https://github.com/seungwonpark/ghudegy-chain/commit/00000000000000c06d2e8c36f247206a9a4b1c63
+
+$ resolve-hash not_a_hash
+$ echo $?
+1
+```
+
+### Why this package?
+
+Terminator has a comprehensive plugins' system to offer extra features,
+like resolve console output as links.
+
+Meanwhile, it's sometimes convenient to open a link in a browser,
+especially if the VCS hash is resolved to the code review system.
+
+### Hash sources
+
+#### VCS
+* Phabricator, browsing your .arcrc file to know the instances you work with
+* Gerrit, if explicitly configured
+* GitHub
+* GitLab, if you provide a token, as search queries must be authenticated
+
+### Configuration
+
+You can provide a configuration by creating a `$HOME/.config/resolve-hash.conf` file.
+
+Configuration is a YAML file.
+
+| Variable | Description | Format |
+|---------------------|-------------------------------|-----------------|
+| gerrit | URL to your Gerrit instances | List of strings |
+| gitlab_public_token | Personal token for GitLab.com | string |
+
+Example:
+
+```yaml
+gerrit:
+ - https://gerrit.wikimedia.org/r/
+
+gitlab_public_token: glpat-sometoken
+```
+
+### Use as a library
+
+You can use the package as a library to resolve hashes in your application:
+
+```python
+from resolvehash.vcs import phabricator
+
+url = phabricator.query_phabricator_instances("/home/luser/.arcrc", "8d8645468228")
+print(url)
+```
+
+### Extend the code
+
+#### How to add a new VCS source?
+
+If you wish to add a new VCS source, add a method in VcsHashSearch,
+then add it to `get_search_methods`.
+
+#### How to add a hash source?
+
+If you wish to extend this script by searching Foo in addition to VCS,
+you can create a class FooHashSearch with the following methods:
+
+ * `__init__(self, config, needle_hash)`: constructor called by the script
+ * `search(self)`: perform your search, return a URL or None
+
+#### How can I contribute?
+
+You can commit your changes to the upstream by following instructions at
+https://agora.nasqueron.org/How_to_contribute_code
+
+The canonical repository is https://devcentral.nasqueron.org/source/resolve-hash.git
+
+### License
+
+BSD-2-Clause, see `LICENSE` file.
diff --git a/bin/resolve-hash b/bin/resolve-hash
new file mode 100755
--- /dev/null
+++ b/bin/resolve-hash
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Query various sources with a known hash
+# like Phabricator, Gerrit or GitHub to offer
+# hash information URL from a VCS hash.
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+import sys
+
+from resolvehash.resolvehash import find_hash, parse_config
+
+
+def run(needle_hash):
+ result = find_hash(parse_config(), needle_hash)
+ if not result:
+ sys.exit(1)
+
+ print(result)
+
+
+if __name__ == "__main__":
+ argc = len(sys.argv)
+ if argc < 2:
+ print(f"Usage: {sys.argv[0]} <hash>", file=sys.stderr)
+ sys.exit(1)
+
+ run(sys.argv[1])
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1 @@
+unittest-data-provider>=1.0.1,<2.0
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+[build-system]
+requires = [
+ "setuptools>=42",
+ "wheel"
+]
+
+build-backend = "setuptools.build_meta"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+PyYAML>=6.0,<7.0
+requests>=2.27.1,<3.0
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,32 @@
+[metadata]
+name = resolve-hash
+version = 0.1.0
+author = Sébastien Santoro
+author_email = dereckson@espace-win.org
+description = Resolve hash
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://devcentral.nasqueron.org/source/resolve-hash/
+project_urls =
+ Bug Tracker = https://devcentral.nasqueron.org/tag/resolve_hash/
+classifiers =
+ Programming Language :: Python :: 3
+ License :: OSI Approved :: BSD License
+ Operating System :: OS Independent
+ Environment :: Console
+ Intended Audience :: Developers
+ Topic :: Software Development :: Version Control
+
+[options]
+package_dir =
+ = src
+packages = find:
+scripts =
+ bin/resolve-hash
+python_requires = >=3.6
+install_requires =
+ PyYAML>=6.0,<7.0
+ requests>=2.27.1,<3.0
+
+[options.packages.find]
+where = src
diff --git a/src/resolvehash/__init__.py b/src/resolvehash/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/__init__.py
@@ -0,0 +1,6 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
diff --git a/src/resolvehash/resolvehash.py b/src/resolvehash/resolvehash.py
new file mode 100755
--- /dev/null
+++ b/src/resolvehash/resolvehash.py
@@ -0,0 +1,54 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Query various sources with a known hash
+# like Phabricator, Gerrit or GitHub to offer
+# hash information URL from a VCS hash.
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import os
+
+import yaml
+
+from resolvehash.search import VcsHashSearch
+
+
+# -------------------------------------------------------------
+# Configuration
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def get_configuration_path():
+ return os.environ["HOME"] + "/.config/resolve-hash.conf"
+
+
+def parse_config():
+ configuration_path = get_configuration_path()
+
+ if not os.path.exists(configuration_path):
+ return {}
+
+ with open(get_configuration_path()) as fd:
+ return yaml.safe_load(fd)
+
+
+# -------------------------------------------------------------
+# Hash search wrapper
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def get_search_classes():
+ return [
+ VcsHashSearch,
+ ]
+
+
+def find_hash(config, needle_hash):
+ for search_class in get_search_classes():
+ result = search_class(config, needle_hash).search()
+
+ if result is not None:
+ return result["url"]
diff --git a/src/resolvehash/search/__init__.py b/src/resolvehash/search/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/search/__init__.py
@@ -0,0 +1,8 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+from .vcs import VcsHashSearch
diff --git a/src/resolvehash/search/vcs.py b/src/resolvehash/search/vcs.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/search/vcs.py
@@ -0,0 +1,62 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Query various sources with a known hash
+# like Phabricator, Gerrit or GitHub to offer
+# hash information URL from a VCS hash.
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import os
+
+from resolvehash.vcs import gerrit
+from resolvehash.vcs import github
+from resolvehash.vcs import gitlab
+from resolvehash.vcs import phabricator
+
+
+class VcsHashSearch:
+ def __init__(self, config, needle_hash):
+ self.config = config
+ self.hash = needle_hash
+
+ def search_phabricator(self):
+ arc_rc_path = os.environ["HOME"] + "/.arcrc"
+ if os.path.exists(arc_rc_path):
+ return phabricator.query_phabricator_instances(arc_rc_path, self.hash)
+
+ def search_gerrit(self):
+ if "gerrit" in self.config:
+ return gerrit.query_gerrit_instances(self.config["gerrit"], self.hash)
+
+ def search_github(self):
+ return github.query_github_instance("https://api.github.com", self.hash)
+
+ def search_gitlab(self):
+ if "gitlab_public_token" in self.config:
+ return gitlab.query_gitlab_instance(
+ "https://gitlab.com/", self.config["gitlab_public_token"], self.hash
+ )
+
+ def get_search_methods(self):
+ return {
+ # Strategy A. Code review systems we can autodiscover
+ "Phabricator": self.search_phabricator,
+ # Strategy B. Sources explicitly configured in configuration
+ "Gerrit": self.search_gerrit,
+ # Strategy C. Popular public hosting sites
+ "GitHub": self.search_github,
+ "GitLab": self.search_gitlab,
+ }
+
+ def search(self):
+ for source, method in self.get_search_methods().items():
+ result = method()
+ if result:
+ return {
+ "search": "vcs",
+ "source": source,
+ "url": result,
+ }
diff --git a/src/resolvehash/vcs/__init__.py b/src/resolvehash/vcs/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/vcs/__init__.py
@@ -0,0 +1,6 @@
+# -------------------------------------------------------------
+# Resolve hash
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
diff --git a/src/resolvehash/vcs/gerrit.py b/src/resolvehash/vcs/gerrit.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/vcs/gerrit.py
@@ -0,0 +1,38 @@
+# -------------------------------------------------------------
+# Resolve hash :: VCS :: Gerrit
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Search hash on Gerrit
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import json
+import requests
+
+
+def query_gerrit_instance(instance, commit_hash):
+ url = instance + "changes/?q=" + commit_hash
+ r = requests.get(url)
+
+ if r.status_code != 200:
+ print(r.status_code)
+ return None
+
+ # We can't use r.json() as currently the API starts responses
+ # by an extra line ")]}'"
+ payload = r.text.strip().split("\n")[-1]
+ result = json.loads(payload)
+
+ if not result:
+ return None
+
+ change = result[0]
+ return f"{instance}c/{change['project']}/+/{change['_number']}"
+
+
+def query_gerrit_instances(instances, commit_hash):
+ for instance in instances:
+ url = query_gerrit_instance(instance, commit_hash)
+ if url is not None:
+ return url
diff --git a/src/resolvehash/vcs/github.py b/src/resolvehash/vcs/github.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/vcs/github.py
@@ -0,0 +1,22 @@
+# -------------------------------------------------------------
+# Resolve hash :: VCS :: GitHub
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Search hash on GitHub
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import requests
+
+
+def query_github_instance(instance, commit_hash):
+ url = f"{instance}/search/commits?q=hash:{commit_hash}"
+ r = requests.get(url)
+
+ if r.status_code != 200:
+ return None
+
+ commits = r.json()["items"]
+ if commits:
+ return commits[0]["html_url"]
diff --git a/src/resolvehash/vcs/gitlab.py b/src/resolvehash/vcs/gitlab.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/vcs/gitlab.py
@@ -0,0 +1,22 @@
+# -------------------------------------------------------------
+# Resolve hash :: VCS :: GitLab
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Search hash on GitLab
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import requests
+
+
+def query_gitlab_instance(instance, token, commit_hash):
+ url = f"{instance}api/v4/search?scope=commits&search={commit_hash}"
+ r = requests.get(url, headers={"PRIVATE-TOKEN": token})
+
+ if r.status_code == 200:
+ return None
+
+ commits = r.json()
+ if commits:
+ return commits[0]["web_url"]
diff --git a/src/resolvehash/vcs/phabricator.py b/src/resolvehash/vcs/phabricator.py
new file mode 100644
--- /dev/null
+++ b/src/resolvehash/vcs/phabricator.py
@@ -0,0 +1,87 @@
+# -------------------------------------------------------------
+# Resolve hash :: VCS :: Phabricator
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Search hash on Phabricator
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import json
+import requests
+
+
+# -------------------------------------------------------------
+# API mechanics
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def call_phabricator_api(api_url, token, method, parameters):
+ parameters["api.token"] = token
+
+ r = requests.post(api_url + method, data=parameters)
+ if r.status_code != 200:
+ return None
+
+ result = r.json()["result"]
+ if result:
+ return result["data"]
+
+
+# -------------------------------------------------------------
+# API methods
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def query_phabricator_instance(api_url, token, commit_hash):
+ result = call_phabricator_api(
+ api_url,
+ token,
+ "diffusion.commit.search",
+ {"constraints[identifiers][0]": commit_hash},
+ )
+
+ if result is None:
+ return None
+
+ try:
+ commit = result[0]
+ except IndexError:
+ # Query works but didn't find anything
+ return None
+
+ return resolve_phabricator_commit_url(
+ api_url, token, commit_hash, commit["fields"]["repositoryPHID"]
+ )
+
+
+def resolve_phabricator_commit_url(api_url, token, commit_hash, repository_phid):
+ callsign = query_get_repository_callsign(api_url, token, repository_phid)
+
+ return api_url.replace("api/", "") + callsign + commit_hash
+
+
+def query_get_repository_callsign(api_url, token, repository_phid):
+ result = call_phabricator_api(
+ api_url,
+ token,
+ "diffusion.repository.search",
+ {"constraints[phids][0]": repository_phid},
+ )
+
+ return "r" + result[0]["fields"]["callsign"]
+
+
+# -------------------------------------------------------------
+# Parse local configuration
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def query_phabricator_instances(config_file, commit_hash):
+ with open(config_file, "r") as fd:
+ instances = json.load(fd)
+
+ for api_url, args in instances["hosts"].items():
+ url = query_phabricator_instance(api_url, args["token"], commit_hash)
+ if url is not None:
+ return url
diff --git a/tests/test_resolvehash.py b/tests/test_resolvehash.py
new file mode 100755
--- /dev/null
+++ b/tests/test_resolvehash.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Resolve hash :: Tests
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# Description: Query various sources with a known hash
+# like Phabricator, Gerrit or GitHub to offer
+# hash information URL from a VCS hash.
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import unittest
+from builtins import staticmethod
+
+from unittest_data_provider import data_provider
+
+from resolvehash import resolvehash
+
+
+class TestResolveHash(unittest.TestCase):
+ def test_get_search_classes(self):
+ search_classes = resolvehash.get_search_classes()
+ self.assertTrue(type(search_classes) is list)
+
+ @staticmethod
+ def search_classes():
+ return (tuple(resolvehash.get_search_classes()),)
+
+ @data_provider(search_classes)
+ def test_if_search_classes_provide_search_method(self, search_class):
+ self.assertTrue(
+ getattr(search_class, "search", None),
+ f"The class `{search_class.__name__}` does NOT implement the method `search`.",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()

File Metadata

Mime Type
text/plain
Expires
Sun, Nov 24, 10:42 (18 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260023
Default Alt Text
D2569.id6490.diff (21 KB)

Event Timeline