Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3768815
D2569.id6490.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
D2569.id6490.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Sun, Nov 24, 10:42 (20 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2260023
Default Alt Text
D2569.id6490.diff (21 KB)
Attached To
Mode
D2569: Resolve VCS hashes to commit URL
Attached
Detach File
Event Timeline
Log In to Comment