diff --git a/main.py b/main.py index c0307a1..c6d3490 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ from io import StringIO import itertools import operator from collections import defaultdict +import sys from Client import Client from GitHubClient import GitHubClient @@ -15,6 +16,98 @@ from LineStatus import LineStatus from LocalClient import LocalClient from TodoParser import TodoParser +def process_diff(diff, client=Client(), insert_issue_urls=False, parser=TodoParser(), output=sys.stdout): + # Parse the diff for TODOs and create an Issue object for each. + raw_issues = parser.parse(diff) + # This is a simple, non-perfect check to filter out any TODOs that have just been moved. + # It looks for items that appear in the diff as both an addition and deletion. + # It is based on the assumption that TODOs will not have identical titles in identical files. + # That is about as good as we can do for TODOs without issue URLs. + issues_to_process = [] + for values, similar_issues_iter in itertools.groupby(raw_issues, key=operator.attrgetter('title', 'file_name', + 'markdown_language')): + similar_issues = list(similar_issues_iter) + if (len(similar_issues) == 2 and all(issue.issue_url is None for issue in similar_issues) + and ((similar_issues[0].status == LineStatus.ADDED + and similar_issues[1].status == LineStatus.DELETED) + or (similar_issues[1].status == LineStatus.ADDED + and similar_issues[0].status == LineStatus.DELETED))): + print(f'Issue "{values[0]}" appears as both addition and deletion. ' + f'Assuming this issue has been moved so skipping.', file=output) + continue + issues_to_process.extend(similar_issues) + + # If a TODO with an issue URL is updated, it may appear as both an addition and a deletion. + # We need to ignore the deletion so it doesn't update then immediately close the issue. + # First store TODOs based on their status. + todos_status = defaultdict(lambda: {'added': False, 'deleted': False}) + + # Populate the status dictionary based on the issue URL. + for raw_issue in issues_to_process: + if raw_issue.issue_url: # Ensuring we're dealing with TODOs that have an issue URL. + if raw_issue.status == LineStatus.ADDED: + todos_status[raw_issue.issue_url]['added'] = True + elif raw_issue.status == LineStatus.DELETED: + todos_status[raw_issue.issue_url]['deleted'] = True + + # Determine which issues are both added and deleted. + update_and_close_issues = set() + + for _issue_url, _status in todos_status.items(): + if _status['added'] and _status['deleted']: + update_and_close_issues.add(_issue_url) + + # Remove issues from issues_to_process if they are both to be updated and closed (i.e., ignore deletions). + issues_to_process = [issue for issue in issues_to_process if + not (issue.issue_url in update_and_close_issues and issue.status == LineStatus.DELETED)] + + # Cycle through the Issue objects and create or close a corresponding GitHub issue for each. + for j, raw_issue in enumerate(sorted(reversed(sorted(issues_to_process, key = operator.attrgetter('start_line'))), key = operator.attrgetter('file_name'))): + print(f"Processing issue {j + 1} of {len(issues_to_process)}: '{raw_issue.title}' @ {raw_issue.file_name}:{raw_issue.start_line}", file=output) + if raw_issue.status == LineStatus.ADDED: + status_code, new_issue_number = client.create_issue(raw_issue) + if status_code == 201: + print(f'Issue created: #{new_issue_number} @ {client.get_issue_url(new_issue_number)}', file=output) + # Don't insert URLs for comments. Comments do not get updated. + if insert_issue_urls and not (raw_issue.ref and raw_issue.ref.startswith('#')): + line_number = raw_issue.start_line - 1 + with open(raw_issue.file_name, 'r') as issue_file: + file_lines = issue_file.readlines() + if line_number < len(file_lines): + # Duplicate the line to retain the comment syntax. + old_line = file_lines[line_number] + remove = fr'(?i:{raw_issue.identifier}).*{re.escape(raw_issue.title)}' + insert = f'Issue URL: {client.get_issue_url(new_issue_number)}' + new_line = re.sub(remove, insert, old_line) + # make sure the above operation worked as intended + if new_line != old_line: + # Check if the URL line already exists, if so abort. + if line_number == len(file_lines) - 1 or file_lines[line_number + 1] != new_line: + file_lines.insert(line_number + 1, new_line) + with open(raw_issue.file_name, 'w') as issue_file: + issue_file.writelines(file_lines) + print('Issue URL successfully inserted', file=output) + else: + print('ERROR: Issue URL was NOT successfully inserted', file=output) + elif status_code == 200: + print(f'Issue updated: #{new_issue_number} @ {client.get_issue_url(new_issue_number)}', file=output) + else: + print('Issue could not be created', file=output) + elif raw_issue.status == LineStatus.DELETED and os.getenv('INPUT_CLOSE_ISSUES', 'true') == 'true': + if raw_issue.ref and raw_issue.ref.startswith('#'): + print('Issue looks like a comment, will not attempt to close.', file=output) + continue + status_code = client.close_issue(raw_issue) + if status_code in [200, 201]: + print('Issue closed', file=output) + else: + print('Issue could not be closed', file=output) + # Stagger the requests to be on the safe side. + sleep(1) + + return raw_issues + + if __name__ == "__main__": client: Client | None = None # Try to create a basic client for communicating with the remote version control server, automatically initialised with environment variables. @@ -30,89 +123,9 @@ if __name__ == "__main__": # Get the diff from the last pushed commit. last_diff = client.get_last_diff() + # process the diff if last_diff: - # Parse the diff for TODOs and create an Issue object for each. - raw_issues = TodoParser().parse(StringIO(last_diff)) - # This is a simple, non-perfect check to filter out any TODOs that have just been moved. - # It looks for items that appear in the diff as both an addition and deletion. - # It is based on the assumption that TODOs will not have identical titles in identical files. - # That is about as good as we can do for TODOs without issue URLs. - issues_to_process = [] - for values, similar_issues_iter in itertools.groupby(raw_issues, key=operator.attrgetter('title', 'file_name', - 'markdown_language')): - similar_issues = list(similar_issues_iter) - if (len(similar_issues) == 2 and all(issue.issue_url is None for issue in similar_issues) - and ((similar_issues[0].status == LineStatus.ADDED - and similar_issues[1].status == LineStatus.DELETED) - or (similar_issues[1].status == LineStatus.ADDED - and similar_issues[0].status == LineStatus.DELETED))): - print(f'Issue "{values[0]}" appears as both addition and deletion. ' - f'Assuming this issue has been moved so skipping.') - continue - issues_to_process.extend(similar_issues) - - # If a TODO with an issue URL is updated, it may appear as both an addition and a deletion. - # We need to ignore the deletion so it doesn't update then immediately close the issue. - # First store TODOs based on their status. - todos_status = defaultdict(lambda: {'added': False, 'deleted': False}) - - # Populate the status dictionary based on the issue URL. - for raw_issue in issues_to_process: - if raw_issue.issue_url: # Ensuring we're dealing with TODOs that have an issue URL. - if raw_issue.status == LineStatus.ADDED: - todos_status[raw_issue.issue_url]['added'] = True - elif raw_issue.status == LineStatus.DELETED: - todos_status[raw_issue.issue_url]['deleted'] = True - - # Determine which issues are both added and deleted. - update_and_close_issues = set() - - for _issue_url, _status in todos_status.items(): - if _status['added'] and _status['deleted']: - update_and_close_issues.add(_issue_url) - - # Remove issues from issues_to_process if they are both to be updated and closed (i.e., ignore deletions). - issues_to_process = [issue for issue in issues_to_process if - not (issue.issue_url in update_and_close_issues and issue.status == LineStatus.DELETED)] - # Check to see if we should insert the issue URL back into the linked TODO. insert_issue_urls = os.getenv('INPUT_INSERT_ISSUE_URLS', 'false') == 'true' - # Cycle through the Issue objects and create or close a corresponding GitHub issue for each. - for j, raw_issue in enumerate(issues_to_process): - print(f"Processing issue {j + 1} of {len(issues_to_process)}: '{raw_issue.title}' @ {raw_issue.file_name}:{raw_issue.start_line}") - if raw_issue.status == LineStatus.ADDED: - status_code, new_issue_number = client.create_issue(raw_issue) - if status_code == 201: - print(f'Issue created: : #{new_issue_number} @ {client.get_issue_url(new_issue_number)}') - # Don't insert URLs for comments. Comments do not get updated. - if insert_issue_urls and not (raw_issue.ref and raw_issue.ref.startswith('#')): - line_number = raw_issue.start_line - 1 - with open(raw_issue.file_name, 'r') as issue_file: - file_lines = issue_file.readlines() - if line_number < len(file_lines): - # Duplicate the line to retain the comment syntax. - new_line = file_lines[line_number] - remove = fr'{raw_issue.identifier}.*{raw_issue.title}' - insert = f'Issue URL: {client.get_issue_url(new_issue_number)}' - new_line = re.sub(remove, insert, new_line) - # Check if the URL line already exists, if so abort. - if line_number == len(file_lines) - 1 or file_lines[line_number + 1] != new_line: - file_lines.insert(line_number + 1, new_line) - with open(raw_issue.file_name, 'w') as issue_file: - issue_file.writelines(file_lines) - elif status_code == 200: - print(f'Issue updated: : #{new_issue_number} @ {client.get_issue_url(new_issue_number)}') - else: - print('Issue could not be created') - elif raw_issue.status == LineStatus.DELETED and os.getenv('INPUT_CLOSE_ISSUES', 'true') == 'true': - if raw_issue.ref and raw_issue.ref.startswith('#'): - print('Issue looks like a comment, will not attempt to close.') - continue - status_code = client.close_issue(raw_issue) - if status_code in [200, 201]: - print('Issue closed') - else: - print('Issue could not be closed') - # Stagger the requests to be on the safe side. - sleep(1) + process_diff(StringIO(last_diff), client, insert_issue_urls) diff --git a/tests/test_comment_suffix_after_source_line.diff b/tests/test_comment_suffix_after_source_line.diff new file mode 100644 index 0000000..d192335 --- /dev/null +++ b/tests/test_comment_suffix_after_source_line.diff @@ -0,0 +1,10 @@ +diff --git a/comment_suffix_after_source_line.c b/comment_suffix_after_source_line.c +new file mode 100644 +index 0000000..d340f6a +--- /dev/null ++++ b/comment_suffix_after_source_line.c +@@ -0,0 +1,4 @@ ++void some_func() { ++ int x = 0; // TODO: give this a better name ++ x++; ++} diff --git a/tests/test_edit.diff b/tests/test_edit.diff new file mode 100644 index 0000000..db98207 --- /dev/null +++ b/tests/test_edit.diff @@ -0,0 +1,14 @@ +diff --git a/tests/example.bas b/tests/example.bas +index 6b0c6cf..b37e70a 100644 +--- a/tests/example.bas ++++ b/tests/example.bas +@@ -1,1 +1,2 @@ ++' TODO: simplify + Public Sub MakeThingsComplicated() +diff --git a/tests/example.frm b/tests/example.frm +index 6b0c6cf..b37e70a 100644 +--- a/tests/example.frm ++++ b/tests/example.frm +@@ -1,1 +1,2 @@ ++' TODO: remove feature to prevent legal issues + Public Sub Plagiarize() diff --git a/tests/test_new.diff b/tests/test_new.diff index 8298cdb..88f647f 100644 --- a/tests/test_new.diff +++ b/tests/test_new.diff @@ -1,8 +1,8 @@ -diff --git a/tests/ExampleFile.java b/tests/ExampleFile.java +diff --git a/ExampleFile.java b/ExampleFile.java new file mode 100644 index 0000000..d340f6a --- /dev/null -+++ b/tests/ExampleFile.java ++++ b/ExampleFile.java @@ -0,0 +1,13 @@ +package com.mydomain.myapp; + @@ -18,11 +18,11 @@ index 0000000..d340f6a + */ +} \ No newline at end of file -diff --git a/tests/example-file.php b/tests/example-file.php +diff --git a/example-file.php b/example-file.php new file mode 100644 index 0000000..063bb80 --- /dev/null -+++ b/tests/example-file.php ++++ b/example-file.php @@ -0,0 +1,23 @@ + + @@ -48,11 +48,11 @@ index 0000000..063bb80 + + \ No newline at end of file -diff --git a/tests/example_file.py b/tests/example_file.py +diff --git a/example_file.py b/example_file.py new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/example_file.py ++++ b/example_file.py @@ -0,0 +1,23 @@ +def hello_world(): + # TODO: Come up with a more imaginative greeting @@ -78,11 +78,11 @@ index 0000000..525e25d + ''' + pass \ No newline at end of file -diff --git a/src/Swarm/Game/example.hs b/src/Swarm/Game/example.hs +diff --git a/example.hs b/example.hs new file mode 100644 index 0000000..0ce9b1a --- /dev/null -+++ b/src/Swarm/Game/example.hs ++++ b/example.hs @@ -0,0 +1,14 @@ +-- | Standard devices that are always installed. +-- @@ -98,23 +98,23 @@ index 0000000..0ce9b1a +TODO: Create an issue for TODO +-} +sum a b = a + b -diff --git a/tests/example_file.cr b/tests/example_file.cr +diff --git a/example_file.cr b/example_file.cr new file mode 100644 index 0000000..e6da2ec --- /dev/null -+++ b/tests/example_file.cr -@@ -0,0 +1,14 @@ ++++ b/example_file.cr +@@ -0,0 +1,6 @@ +# TODO: Come up with a more imaginative greeting +puts "Greetings" + +# TODO: Do even more stuff +# This function should probably do something more interesting +# labels: help wanted -diff --git a/tests/example_file.rb b/tests/example_file.rb +diff --git a/example_file.rb b/example_file.rb new file mode 100644 index 0000000..e6da2ec --- /dev/null -+++ b/tests/example_file.rb ++++ b/example_file.rb @@ -0,0 +1,14 @@ +#!/usr/bin/ruby -w + @@ -130,11 +130,11 @@ index 0000000..e6da2ec +# TODO: Do even more stuff +# This function should probably do something more interesting +# labels: help wanted -diff --git a/tests/example_file.yaml b/tests/example_file.yaml +diff --git a/example_file.yaml b/example_file.yaml new file mode 100644 index 0000000..6397789 --- /dev/null -+++ b/tests/example_file.yaml ++++ b/example_file.yaml @@ -0,0 +1,7 @@ +name: "TODO to Issue" +# TODO: Write a more interesting description @@ -143,11 +143,11 @@ index 0000000..6397789 +# TODO: Define inputs +# Need to do this before the action is released +# labels: urgent -diff --git a/tests/example_file.toml b/tests/example_file.toml +diff --git a/example_file.toml b/example_file.toml new file mode 100644 index 0000000..6397789 --- /dev/null -+++ b/tests/example_file.toml ++++ b/example_file.toml @@ -0,0 +1,7 @@ +name = "TODO to Issue" +# TODO: Write a more interesting description @@ -156,31 +156,31 @@ index 0000000..6397789 +# TODO: Define inputs +# Need to do this before the action is released +# labels: urgent -diff --git a/tests/example_file.prog.abap b/src/tests/example_file.prog.abap +diff --git a/example_file.prog.abap b/example_file.prog.abap new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/tests/example_file.prog.abap ++++ b/example_file.prog.abap @@ -0,0 +1,5 @@ +REPORT ztest_todo_2. + +DATA moo TYPE i VALUE 2. +WRITE moo. " TODO This is an end-of-line to-do +moo = 4. * TODO This is another end-of-line to-do -diff --git a/tests/example_file.sql b/src/tests/example_file.sql +diff --git a/example_file.sql b/example_file.sql new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.sql ++++ b/example_file.sql @@ -0,0 +1,2 @@ +-- TODO Select all: +SELECT * FROM Products; -diff --git a/tests/example_file.tex b/src/tests/example_file.tex +diff --git a/example_file.tex b/example_file.tex new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.tex -@@ -0,0 +1,2 @@ ++++ b/example_file.tex +@@ -0,0 +1,7 @@ +% TODO Add in preamble details +\begin{document} + \begin{comment} @@ -188,24 +188,24 @@ index 0000000..7cccc5b + label: urgent + \end{comment} +\end{document} -diff --git a/tests/example_file.jl b/src/tests/example_file.jl +diff --git a/example_file.jl b/example_file.jl new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.jl -@@ -0,0 +1,2 @@ ++++ b/example_file.jl +@@ -0,0 +1,6 @@ + # TODO: Hopefully this comment turns into an issue + print("Hello World") + #= TODO: Multiline comments + also need to be turned into task, and hopefully + kept together as one. + =# -diff --git a/tests/defs.bzl b/tests/defs.bzl +diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/defs.bzl -@@ -0,0 +1,23 @@ ++++ b/defs.bzl +@@ -0,0 +1,8 @@ +def hello_world(): + # TODO: Come up with a more imaginative greeting + print('Hello world') @@ -214,12 +214,12 @@ index 0000000..525e25d + # This function should probably do something more interesting + # labels: help wanted + pass -diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel +diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/BUILD.bazel -@@ -0,0 +1,23 @@ ++++ b/BUILD.bazel +@@ -0,0 +1,8 @@ +def hello_world(): + # TODO: Come up with a more imaginative greeting + print('Hello world') @@ -228,33 +228,33 @@ index 0000000..525e25d + # This function should probably do something more interesting + # labels: help wanted + pass -diff --git a/tests/example_file.ahk b/src/tests/example_file.ahk +diff --git a/example_file.ahk b/example_file.ahk new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.ahk ++++ b/example_file.ahk @@ -0,0 +1,2 @@ + ; TODO: Find a better way to manage hotkeys + ; Maybe just switch to vim?? + #h:: + RegRead, HiddenFiles_Status, HKEY_CURRENT_USER, Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced, Hidden -diff --git a/tests/example_file.hbs b/src/tests/example_file.hbs +diff --git a/example_file.hbs b/example_file.hbs new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.hbs -@@ -0,0 +1,2 @@ ++++ b/example_file.hbs +@@ -0,0 +1,5 @@ + + {{! + TODO: Make a handlebar templtate + This is really just a test, but hopefully this works~! + }} -diff --git a/tests/example_file.org b/src/tests/example_file.org +diff --git a/example_file.org b/example_file.org new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.org -@@ -0,0 +1,2 @@ ++++ b/example_file.org +@@ -0,0 +1,9 @@ + # TODO: Hopefully this comment turns into a todo issue + #+begin_src python + print("Hello World") @@ -264,11 +264,11 @@ index 0000000..7cccc5b + also need to be turned into todos, and hopefully + kept together as one todo + #+end_comment -diff --git a/tests/example_file.scss b/src/tests/example_file.scss +diff --git a/example_file.scss b/example_file.scss new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.scss ++++ b/example_file.scss @@ -0,0 +1,11 @@ +// TODO: Hopefully this comment turns into a todo issue +.my-class { @@ -281,11 +281,11 @@ index 0000000..7cccc5b + text-align: center; + } +} -diff --git a/tests/example_file.twig b/src/tests/example_file.twig +diff --git a/example_file.twig b/example_file.twig new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.twig ++++ b/example_file.twig @@ -0,0 +1,6 @@ + {# TODO: Hopefully this comment turns into a todo issue #} + {# @@ -293,11 +293,11 @@ index 0000000..7cccc5b + also need to be turned into todos, and hopefully + kept together as one todo + #} -diff --git a/tests/example_file.md b/src/tests/example_file.md +diff --git a/example_file.md b/example_file.md new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.md ++++ b/example_file.md @@ -0,0 +1,7 @@ + {/* TODO: Hopefully this comment turns into a todo issue */} + {/* @@ -306,11 +306,11 @@ index 0000000..7cccc5b + kept together as one todo + */} + - [ ] TODO: An inline todo that's NOT a comment (what) -diff --git a/tests/example_file.mdx b/src/tests/example_file.mdx +diff --git a/example_file.mdx b/example_file.mdx new file mode 100644 index 0000000..7cccc5b --- /dev/null -+++ b/src/tests/example_file.mdx ++++ b/example_file.mdx @@ -0,0 +1,7 @@ + {/* TODO: Hopefully this comment turns into a todo issue */} + {/* @@ -319,12 +319,12 @@ index 0000000..7cccc5b + kept together as one todo + */} + - [ ] TODO: An inline todo that's NOT a comment (what) -diff --git a/tests/example_file.r b/tests/example_file.r +diff --git a/example_file.r b/example_file.r new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/example_file.r -@@ -0,0 +1,23 @@ ++++ b/example_file.r +@@ -0,0 +1,8 @@ +hello_world <- function() { + # TODO: Come up with a more imaginative greeting + message("Hello world") @@ -333,56 +333,25 @@ index 0000000..525e25d + # This function should probably do something more interesting + # labels: help wanted +} -diff --git a/tests/example_file.rmd b/src/tests/example_file.rmd +diff --git a/example_file.rmd b/example_file.rmd new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/src/tests/example_file.rmd ++++ b/example_file.rmd @@ -0,0 +0,1 @@ + -diff --git a/tests/example_file.qmd b/src/tests/example_file.qmd +diff --git a/example_file.qmd b/example_file.qmd new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/src/tests/example_file.qmd ++++ b/example_file.qmd @@ -0,0 +0,1 @@ + -diff --git a/tests/config.jsonc b/tests/config.jsonc +diff --git a/example.clj b/example.clj new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/config.jsonc -@@ -0,0 +0,11 @@ -+{ -+ "myConfig": [ -+ /* -+ GitRepository where 'Git release/tag' -+ matches 'Helm' version -+ */ -+ "itIsWonderful", -+ // TODO: Delete this line from the codebase -+ "butItHasSomePendingActivities" -+ ] -+} -diff --git a/tests/config.json5 b/tests/config.json5 -new file mode 100644 -index 0000000..525e25d ---- /dev/null -+++ b/tests/config.json5 -@@ -0,0 +0,8 @@ -+{ -+ "myConfig": [ -+ // GitRepository where 'Git release/tag' matches 'Helm' version -+ "itIsWonderful", -+ // TODO: Delete this line from the codebase -+ "butItHasSomePendingActivities" -+ ] -+} -diff --git a/tests/example.clj b/tests/example.clj -new file mode 100644 -index 0000000..525e25d ---- /dev/null -+++ b/tests/example.clj ++++ b/example.clj @@ -0,0 +1,8 @@ +(ns example) + @@ -392,22 +361,22 @@ index 0000000..525e25d + ;; TODO: todo with description (sub-level) + ;; Body of the issue should stay here + (str "hello" name)) -diff --git a/tests/example.gd b/tests/example.gd +diff --git a/example.gd b/example.gd new file mode 100644 index 0000000..525e25d --- /dev/null -+++ b/tests/example.gd ++++ b/example.gd @@ -0,0 +1,5 @@ +# TODO Store all the players here +var _players: = [] + +# TODO Play the level music +export: var music_path: String -diff --git a/tests/example.nix b/tests/example.nix +diff --git a/example.nix b/example.nix new file mode 100644 index 0000000..a6c6cb0 --- /dev/null -+++ b/tests/example.nix ++++ b/example.nix @@ -0,0 +1,11 @@ +{ + # TODO add missing devices @@ -420,36 +389,22 @@ index 0000000..a6c6cb0 + */ + sum = a: b: a + b; +} -diff --git a/tests/example.xaml b/tests/example.xaml +diff --git a/example.xaml b/example.xaml new file mode 100644 index 0000000..a6c6cb0 --- /dev/null -+++ b/tests/example.xaml ++++ b/example.xaml @@ -0,0 +1,4 @@ + +