1"""
2tags for common git attributes
3"""
4import os
5import subprocess
6from typing import Dict
7from typing import MutableMapping
8from typing import Optional
9from typing import Tuple
10
11import six
12
13from ddtrace.internal import compat
14from ddtrace.internal.logger import get_logger
15
16
17if six.PY2:
18    GitNotFoundError = OSError
19else:
20    GitNotFoundError = FileNotFoundError
21
22# Git Branch
23BRANCH = "git.branch"
24
25# Git Commit SHA
26COMMIT_SHA = "git.commit.sha"
27
28# Git Repository URL
29REPOSITORY_URL = "git.repository_url"
30
31# Git Tag
32TAG = "git.tag"
33
34# Git Commit Author Name
35COMMIT_AUTHOR_NAME = "git.commit.author.name"
36
37# Git Commit Author Email
38COMMIT_AUTHOR_EMAIL = "git.commit.author.email"
39
40# Git Commit Author Date (UTC)
41COMMIT_AUTHOR_DATE = "git.commit.author.date"
42
43# Git Commit Committer Name
44COMMIT_COMMITTER_NAME = "git.commit.committer.name"
45
46# Git Commit Committer Email
47COMMIT_COMMITTER_EMAIL = "git.commit.committer.email"
48
49# Git Commit Committer Date (UTC)
50COMMIT_COMMITTER_DATE = "git.commit.committer.date"
51
52# Git Commit Message
53COMMIT_MESSAGE = "git.commit.message"
54
55log = get_logger(__name__)
56
57
58def _git_subprocess_cmd(cmd, cwd=None):
59    # type: (str, Optional[str]) -> str
60    """Helper for invoking the git CLI binary."""
61    git_cmd = cmd.split(" ")
62    git_cmd.insert(0, "git")
63    process = subprocess.Popen(git_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
64    stdout, stderr = process.communicate()
65    if process.returncode == 0:
66        return compat.ensure_text(stdout).strip()
67    raise ValueError(stderr)
68
69
70def extract_user_info(cwd=None):
71    # type: (Optional[str]) -> Dict[str, Tuple[str, str, str]]
72    """Extract commit author info from the git repository in the current directory or one specified by ``cwd``."""
73    # Note: `git show -s --format... --date...` is supported since git 2.1.4 onwards
74    stdout = _git_subprocess_cmd("show -s --format=%an,%ae,%ad,%cn,%ce,%cd --date=format:%Y-%m-%dT%H:%M:%S%z", cwd=cwd)
75    author_name, author_email, author_date, committer_name, committer_email, committer_date = stdout.split(",")
76    return {
77        "author": (author_name, author_email, author_date),
78        "committer": (committer_name, committer_email, committer_date),
79    }
80
81
82def extract_repository_url(cwd=None):
83    # type: (Optional[str]) -> str
84    """Extract the repository url from the git repository in the current directory or one specified by ``cwd``."""
85    # Note: `git show ls-remote --get-url` is supported since git 2.6.7 onwards
86    repository_url = _git_subprocess_cmd("ls-remote --get-url", cwd=cwd)
87    return repository_url
88
89
90def extract_commit_message(cwd=None):
91    # type: (Optional[str]) -> str
92    """Extract git commit message from the git repository in the current directory or one specified by ``cwd``."""
93    # Note: `git show -s --format... --date...` is supported since git 2.1.4 onwards
94    commit_message = _git_subprocess_cmd("show -s --format=%s", cwd=cwd)
95    return commit_message
96
97
98def extract_workspace_path(cwd=None):
99    # type: (Optional[str]) -> str
100    """Extract the root directory path from the git repository in the current directory or one specified by ``cwd``."""
101    workspace_path = _git_subprocess_cmd("rev-parse --show-toplevel", cwd=cwd)
102    return workspace_path
103
104
105def extract_branch(cwd=None):
106    # type: (Optional[str]) -> str
107    """Extract git branch from the git repository in the current directory or one specified by ``cwd``."""
108    branch = _git_subprocess_cmd("rev-parse --abbrev-ref HEAD", cwd=cwd)
109    return branch
110
111
112def extract_commit_sha(cwd=None):
113    # type: (Optional[str]) -> str
114    """Extract git commit SHA from the git repository in the current directory or one specified by ``cwd``."""
115    commit_sha = _git_subprocess_cmd("rev-parse HEAD", cwd=cwd)
116    return commit_sha
117
118
119def extract_git_metadata(cwd=None):
120    # type: (Optional[str]) -> Dict[str, Optional[str]]
121    """Extract git commit metadata."""
122    tags = {}  # type: Dict[str, Optional[str]]
123    try:
124        tags[REPOSITORY_URL] = extract_repository_url(cwd=cwd)
125        tags[COMMIT_MESSAGE] = extract_commit_message(cwd=cwd)
126        users = extract_user_info(cwd=cwd)
127        tags[COMMIT_AUTHOR_NAME] = users["author"][0]
128        tags[COMMIT_AUTHOR_EMAIL] = users["author"][1]
129        tags[COMMIT_AUTHOR_DATE] = users["author"][2]
130        tags[COMMIT_COMMITTER_NAME] = users["committer"][0]
131        tags[COMMIT_COMMITTER_EMAIL] = users["committer"][1]
132        tags[COMMIT_COMMITTER_DATE] = users["committer"][2]
133        tags[BRANCH] = extract_branch(cwd=cwd)
134        tags[COMMIT_SHA] = extract_commit_sha(cwd=cwd)
135    except GitNotFoundError:
136        log.error("Git executable not found, cannot extract git metadata.")
137    except ValueError:
138        log.error("Error extracting git metadata, received non-zero return code.", exc_info=True)
139
140    return tags
141
142
143def extract_user_git_metadata(env=None):
144    # type: (Optional[MutableMapping[str, str]]) -> Dict[str, Optional[str]]
145    """Extract git commit metadata from user-provided env vars."""
146    env = os.environ if env is None else env
147
148    tags = {}
149    tags[REPOSITORY_URL] = env.get("DD_GIT_REPOSITORY_URL")
150    tags[COMMIT_SHA] = env.get("DD_GIT_COMMIT_SHA")
151    tags[BRANCH] = env.get("DD_GIT_BRANCH")
152    tags[TAG] = env.get("DD_GIT_TAG")
153    tags[COMMIT_MESSAGE] = env.get("DD_GIT_COMMIT_MESSAGE")
154    tags[COMMIT_AUTHOR_DATE] = env.get("DD_GIT_COMMIT_AUTHOR_DATE")
155    tags[COMMIT_AUTHOR_EMAIL] = env.get("DD_GIT_COMMIT_AUTHOR_EMAIL")
156    tags[COMMIT_AUTHOR_NAME] = env.get("DD_GIT_COMMIT_AUTHOR_NAME")
157    tags[COMMIT_COMMITTER_DATE] = env.get("DD_GIT_COMMIT_COMMITTER_DATE")
158    tags[COMMIT_COMMITTER_EMAIL] = env.get("DD_GIT_COMMIT_COMMITTER_EMAIL")
159    tags[COMMIT_COMMITTER_NAME] = env.get("DD_GIT_COMMIT_COMMITTER_NAME")
160
161    return tags
162