Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

# generated and other user related data
json_dump/
user_image_dir/
config.py
gource-input.txt
missing-tickets.txt
names.txt

# python folder
__pycache__
44 changes: 44 additions & 0 deletions FIX-VERSION-HIERARCHY-CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Build deep hierarchy by fixVersion

In the `config.sample-hierarchy.apache-zeppeline.py` present an alternative configuration with some additional methods to build tiket issue path with all previous fix versions.

### How it work

Task `PROJECT-1` with the first fix version `1.0` will have path `PROJECT/1.0/PROJECT-1.Task` and all other tickets with that version also have that path (except the latest part), so all tiket will be group nearly
Another ticket with version `1.1` will include path from previous version, so Task `PROJECT-100` with this version will have path: `PROJECT/1.0/1.1/PROJECT-100.Task`
For the next version everything will work the same: `1.0/1.2/1.3/2.0/3.0/3.1`, but hotfix version (this a version with the third non-zero part like `1.0.1` will use parent part, but dont affect the whole hiearchy, will be like side branch as `0.6.2` in the picture bellow)

So we have a long chain of version and long history/graphic :) on the screen like
![sample of long chain](https://github.com/igubanov/jira-gource/assets/133564049/88d96a98-ba77-4e10-be90-7bc767a77a28)
and even longer.

### How it use

You should copy `config.sample-hierarchy.apache-zeppeline.py` to `config.py` and replace value for the next variables:

- `jira_url`
- `jira_server_project`
- `projects`
- `my_user_name`

and execute script

```
python -m pip install -r requirements.txt
python generate_gource.py
```

### Some tips for generate

A history line builded for project may be very long, by default gource show all folder/path/files on the screen make zoom is small, by prevent this action we can use combination of parameters `--camera-mode track` to keep focus on users (they are ussually near latest version) and `--padding 1.9` to have more space near the users (that's a workaround for zoom)

### Demo

This demo is builded from Appache/Zeppelin [public jira](https://issues.apache.org/jira/projects/ZEPPELIN/issues)
with the next generation's parameters:

```
gource -1280x720 -c 2 -s 1 --start-date "2019-01-01" --stop-date "2019-03-01" --hide filenames --user-image-dir user_image_dir --camera-mode track --key --padding 1.9 -o - gource-input.txt
```

https://github.com/igubanov/jira-gource/assets/133564049/a1f4ea32-5ce3-4750-8b84-ce1cde4e3fed
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ ticket PROJECT-42 with summary "System: Component: implement feature" will be
represented by `PROJECT/System/Component/PROJECT-42.Task` in the generated
Gource input.

### Alternative way to build hierarchy via fixVersion

By default hierarchy of ticket (path) builded by **_jira components_** value and **_section_** (part of summary split by `:`, _details [here](https://github.com/rybak/jira-gource/blob/00acf0c78265eb9492339185e710359264e5e2d5/history_converter.py#L25C5-L28C61)_)

But what if we want to see history project through it evalution by release/fix version? So then [check this](FIX-VERSION-HIERARCHY-CONFIG.md)

### Dependencies and compatibility

* [requests](http://python-requests.org) library – to talk to a Jira server
Expand Down
112 changes: 112 additions & 0 deletions config.sample-hierarchy.apache-zeppeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from datetime import date
from configlib import *

# user name can be empty or with any value when use token authentification for jira server (token works without login)
my_user_name = "anonymous"
jira_url = "https://issues.apache.org/jira"

# Change this to a path to the certificate for your server or to False to
# completely ignore verification. For more info see
# <http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification>
verify = True


def skip_filter(changelog_entry, issue_json) -> bool:
"""
Predicate to tell whether or not specified changelog entry should be
skipped.

:param changelog_entry:
The changelog entry of an issue to examine.
:param issue_json
JSON object of a ticket, as per GET "issue" request of JIRA's REST API.
:returns:
A boolean, True to skip entry, False to keep it.
"""
fixVersion = get_compound_jira_field(issue_json, "fixVersions", several_name="Multiple")

# skip issues with non concrete versions
if fixVersion in [None, "Multiple"]:
return True;

# side effect - save all uniq fix version for building hierarchy later
if fixVersion not in allFixVersions:
allFixVersions.append(fixVersion)

return False

# Specify a way to extend the default sections of a ticket
JIRA_SERVER_COMPONENTS_KEY = 'components'
FIX_VERSIONS_KEY = 'fixVersions'

# internal global variables for building hierarchy via fix versions
allFixVersions = []
hierarchicalFixVersions = {}

def sections_extension_jira_server(issue_json, sections):
"""
Generate custom list of folders for the file of the ticket in the Gource
input. Component, which the ticket is related to, is used as a folder.
Folder named "Multiple" is used when there are more than one components.
When component is not defined, no additional folders are used.

:param issue_json:
JSON object of a ticket, as per GET "issue" request of JIRA's REST API.
:param sections:
Default list of folders, generated from summary of the ticket.
:return:
Customized list of folders.
"""
extra_sections = []
fixVersion = get_compound_jira_field(issue_json, FIX_VERSIONS_KEY, several_name="Multiple")

_build_hierarchy_fix_versions()
if fixVersion:
for version_part in hierarchicalFixVersions.get(fixVersion).split("/"):
extra_sections.append(version_part)

return extra_sections

def _build_hierarchy_fix_versions():
"""
Build hierarchical path for fix versions into hierarchicalFixVersions dict
previos version (and its path) should be used for next version
deep 2 lvl 1.0, 1.1 will be like 1.0/1.1, but 1.1.1 (1.0/1.1/1.1.1) wont be parent for 1.2, just 1.0/1.1/1.2
except hotfix version - they always child of its parent
"""
already_filled = len(hierarchicalFixVersions.keys()) > 0
if already_filled:
return

get_major_minor_part = lambda s: list(map(int, s.split('.')[:2]))
previous_version = None
for version in sorted(allFixVersions, key=get_major_minor_part):
previous_path = hierarchicalFixVersions.get(previous_version)
if previous_path:
current_version_path = '{}/{}'.format(previous_path, version)
else:
current_version_path = version

hierarchicalFixVersions.update({version: current_version_path})

is_hotfix_version = len(version.split('.')) > 2 and version.split('.')[2] != '0'
if is_hotfix_version is not True:
previous_version = version

print('hierarchy is builded')


jira_server_project = {
'min_key': 1,
'max_key': 6000,
'skip_dates': {
date(1980, 1, 1), # big ticket move
},
'skip_filter': skip_filter,
'sections_extension': sections_extension_jira_server,
'extra_fields': [FIX_VERSIONS_KEY]
}

projects = {
'ZEPPELIN': jira_server_project
}
10 changes: 8 additions & 2 deletions config.sample.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import date
from configlib import *

# user name can be empty or with any value when use token authentification for jira server (token works without login)
my_user_name = "johnsmith"
jira_url = "https://jira.atlassian.com"

Expand All @@ -10,19 +11,24 @@
verify = True


def skip_filter(changelog_entry) -> bool:
def skip_filter(changelog_entry, issue_json) -> bool:
"""
Predicate to tell whether or not specified changelog entry should be
skipped.

:param changelog_entry:
The changelog entry of an issue to examine.
:param issue_json
JSON object of a ticket, as per GET "issue" request of JIRA's REST API.
:returns:
A boolean, True to skip entry, False to keep it.
"""

isTask = get_jira_field(issue_json, "issuetype") == "Task"

# this skip_filter skips any changelog entry which changes Workflow of
# a ticket
return is_field_change(changelog_entry, 'Workflow')
return is_field_change(changelog_entry, 'Workflow') or isTask


# Specify a way to extend the default sections of a ticket
Expand Down
28 changes: 26 additions & 2 deletions configlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ def get_compound_custom_field(issue_json, field_key: str,
return _get_compound_field(issue_json, field_key, default_value,
several_name, 'value')

def get_raw_custom_field(issue_json, field_key: str,
default_value: str = None,
multiple_value: str = None) -> str:
"""Get a raw value from a custom plugin field of an issue.
for example:
"customfield_10000": [
"com.atlassian.greenhopper.service..@6c5f4a91[id=9129,rapidViewId=1432,state=CLOSED,name= Name 1,startDate=2021-11-22T14:03:00.000+03:00,endDate=2021-12-06T14:03:00.000+03:00,completeDate=2021-12-07T11:08:56.178+03:00,activatedDate=2021-11-22T16:42:48.614+03:00,sequence=8164,goal=some description about a goal,autoStartStop=false]",
"com.atlassian.greenhopper.service..@44220ba2[id=9206,rapidViewId=1432,state=CLOSED,name= name 2,startDate=2021-12-06T18:00:00.000+03:00,endDate=2021-12-20T18:00:00.000+03:00,completeDate=2021-12-21T13:16:54.613+03:00,activatedDate=2021-12-07T11:09:33.366+03:00,sequence=8343,goal=finish client functional,autoStartStop=false]"
]

:param issue_json:
The JSON object to examine, which represents one issue (ticket),
returned by method `issue` of JIRA REST API.
:param field_key:
JSON key of the custom field.
:param default_value:
String to return, when the field is absent or its value is not defined.
:param multiple_value:
String to return, when the field has more than one value, if None - then return first value.
"""
return_first_value = multiple_value == None
return _get_compound_field(issue_json, field_key, default_value, multiple_value, return_first_value = return_first_value)


def is_field_change(changelog_entry, field_key: str) -> bool:
"""Check if changelog entry contains field value change.
Expand Down Expand Up @@ -111,11 +134,12 @@ def _get_field(issue_json, field_key: str, default_value: str = None,

def _get_compound_field(issue_json, field_key: str, default_value: str = None,
several_name: str = "Several",
subscript: str = None) -> str:
subscript: str = None,
return_first_value: bool = False) -> str:
field = _extract_field(issue_json, field_key)
if field is None:
return default_value
if len(field) == 1:
if len(field) == 1 or (return_first_value and len(field) > 0):
if subscript:
return field[0][subscript]
return field[0]
Expand Down
Loading