mirror of
https://github.com/Relintai/pandemonium_engine_easy_charts.git
synced 2024-12-29 14:17:18 +01:00
568 lines
21 KiB
GDScript
568 lines
21 KiB
GDScript
# ----------------------------------------------
|
|
# ~{ GitHub Integration }~
|
|
# [Author] Nicolò "fenix" Santilio
|
|
# [github] fenix-hub/godot-engine.github-integration
|
|
# [version] 0.2.9
|
|
# [date] 09.13.2019
|
|
|
|
|
|
# Committing this repo I always need to delete all .import files
|
|
# and don't import this gitignore.
|
|
|
|
|
|
# -----------------------------------------------
|
|
|
|
tool
|
|
extends Control
|
|
|
|
|
|
onready var _message = $VBoxContainer2/HBoxContainer7/message
|
|
#onready var _file = $VBoxContainer/HBoxContainer/file
|
|
onready var _branch = $VBoxContainer2/HBoxContainer2/branch
|
|
onready var repository = $VBoxContainer2/HBoxContainer/repository
|
|
onready var Loading = $VBoxContainer2/loading2
|
|
|
|
onready var Gitignore = $VBoxContainer2/HBoxContainer3/VBoxContainer2/gitignore
|
|
onready var gitignoreBtn = $VBoxContainer2/HBoxContainer3/VBoxContainer2/girignorebuttons/gitignoreBtn
|
|
onready var about_gitignoreBtn = $VBoxContainer2/HBoxContainer3/VBoxContainer2/girignorebuttons/about_gitignoreBtn
|
|
|
|
onready var SelectFiles = $ChooseFile
|
|
onready var selectfilesBtn = $VBoxContainer2/HBoxContainer3/VBoxContainer/HBoxContainer/choosefilesBtn
|
|
onready var removefileBtn = $VBoxContainer2/HBoxContainer3/VBoxContainer/HBoxContainer/removefileBtn
|
|
onready var selectdirectoryBtn = $VBoxContainer2/HBoxContainer3/VBoxContainer/HBoxContainer/choosedirectoryBtn
|
|
|
|
onready var Progress = $VBoxContainer2/ProgressBar
|
|
|
|
onready var Uncommitted = $VBoxContainer2/HBoxContainer3/VBoxContainer/uncommitted
|
|
|
|
enum REQUESTS { UPLOAD = 0, UPDATE = 1, BLOB = 2 , LATEST_COMMIT = 4, BASE_TREE = 5, NEW_TREE = 8, NEW_COMMIT = 6, PUSH = 7, COMMIT = 9, LFS = 10, POST_LFS = 11, END = -1 }
|
|
var requesting
|
|
var new_repo = HTTPRequest.new()
|
|
|
|
var repo_selected
|
|
var branches = []
|
|
var branches_contents = []
|
|
var branch_idx = 0
|
|
|
|
var files : Array = []
|
|
var directories = []
|
|
|
|
onready var error = $VBoxContainer2/error
|
|
|
|
var sha_latest_commit
|
|
var sha_base_tree
|
|
var sha_new_tree
|
|
var sha_new_commit
|
|
|
|
var list_file_sha = []
|
|
var list_file_size = []
|
|
var lfs = []
|
|
|
|
var gitignore_file : Dictionary
|
|
|
|
const DIRECTORY : String = "res://"
|
|
|
|
var IGNORE_FILES : PoolStringArray = []
|
|
var IGNORE_FOLDERS : PoolStringArray = []
|
|
|
|
var current_handled_file : String
|
|
|
|
signal blob_created()
|
|
|
|
signal latest_commit()
|
|
signal base_tree()
|
|
signal new_commit()
|
|
signal new_tree()
|
|
signal file_blobbed()
|
|
signal file_committed()
|
|
signal pushed()
|
|
signal files_filtered()
|
|
signal lfs()
|
|
signal lfs_push()
|
|
|
|
func _ready():
|
|
new_repo.use_threads = true
|
|
connect_signals()
|
|
|
|
Loading.hide()
|
|
call_deferred("add_child",new_repo)
|
|
error.hide()
|
|
|
|
removefileBtn.set_disabled(true)
|
|
|
|
func set_darkmode(darkmode : bool):
|
|
if darkmode:
|
|
$BG.color = "#24292e"
|
|
set_theme(load("res://addons/github-integration/resources/themes/GitHubTheme-Dark.tres"))
|
|
else:
|
|
$BG.color = "#f6f8fa"
|
|
set_theme(load("res://addons/github-integration/resources/themes/GitHubTheme.tres"))
|
|
|
|
func connect_signals():
|
|
new_repo.connect("request_completed",self,"request_completed")
|
|
_branch.connect("item_selected",self,"selected_branch")
|
|
gitignoreBtn.connect("toggled",self,"on_gitignore_toggled")
|
|
selectfilesBtn.connect("pressed",self,"on_selectfiles_pressed")
|
|
# SelectFiles.connect("confirmed",self,"on_confirm")
|
|
SelectFiles.connect("files_selected",self,"on_files_selected")
|
|
Uncommitted.connect("item_selected",self,"on_item_selected")
|
|
Uncommitted.connect("multi_selected",self,"on_multiple_item_selected")
|
|
removefileBtn.connect("pressed",self,"on_removefile_pressed")
|
|
Uncommitted.connect("nothing_selected",self,"on_nothing_selected")
|
|
SelectFiles.connect("dir_selected",self,"on_dir_selected")
|
|
selectdirectoryBtn.connect("pressed",self,"on_selectdirectory_pressed")
|
|
about_gitignoreBtn.connect("pressed",self,"about_gitignore_pressed")
|
|
|
|
func request_completed(result, response_code, headers, body ):
|
|
get_parent().print_debug_message("REQUEST TO API : Request exited with code %s" % response_code)
|
|
if response_code == 422:
|
|
get_parent().print_debug_message(JSON.parse(body.get_string_from_utf8()).result)
|
|
if result == 0:
|
|
match requesting:
|
|
REQUESTS.UPLOAD:
|
|
if response_code == 201:
|
|
hide()
|
|
get_parent().print_debug_message("commited and pushed...")
|
|
get_parent().UserPanel.request_repositories(get_parent().UserPanel.REQUESTS.UP_REPOS)
|
|
elif response_code == 422:
|
|
error.text = "Error: "+JSON.parse(body.get_string_from_utf8()).result.errors[0].message
|
|
error.show()
|
|
REQUESTS.UPDATE:
|
|
if response_code == 200:
|
|
pass
|
|
REQUESTS.COMMIT:
|
|
if response_code == 201:
|
|
get_parent().print_debug_message("file committed!")
|
|
get_parent().print_debug_message(" ")
|
|
emit_signal("file_committed")
|
|
if response_code == 200:
|
|
get_parent().print_debug_message("file updated!")
|
|
get_parent().print_debug_message(" ")
|
|
emit_signal("file_committed")
|
|
if response_code == 422:
|
|
get_parent().print_debug_message("file already exists, skipping...")
|
|
get_parent().print_debug_message(" ")
|
|
emit_signal("file_committed")
|
|
REQUESTS.LATEST_COMMIT:
|
|
if response_code == 200:
|
|
sha_latest_commit = JSON.parse(body.get_string_from_utf8()).result.object.sha
|
|
get_parent().print_debug_message("got last commit")
|
|
emit_signal("latest_commit")
|
|
REQUESTS.BASE_TREE:
|
|
if response_code == 200:
|
|
sha_base_tree = JSON.parse(body.get_string_from_utf8()).result.tree.sha
|
|
get_parent().print_debug_message("got base tree")
|
|
emit_signal("base_tree")
|
|
REQUESTS.BLOB:
|
|
match response_code:
|
|
201:
|
|
list_file_sha.append(JSON.parse(body.get_string_from_utf8()).result.sha)
|
|
get_parent().print_debug_message("blobbed file")
|
|
# OS.delay_msec(1000)
|
|
emit_signal("file_blobbed")
|
|
400:
|
|
list_file_sha.append("no-sha")
|
|
get_parent().print_debug_message("could not blob this file due to reading errors, skipping...",1)
|
|
emit_signal("file_blobbed")
|
|
502:
|
|
list_file_sha.append("no-sha")
|
|
get_parent().print_debug_message("could not blob this file due to server errors, skipping...",1)
|
|
emit_signal("file_blobbed")
|
|
REQUESTS.NEW_TREE:
|
|
match response_code:
|
|
201:
|
|
sha_new_tree = JSON.parse(body.get_string_from_utf8()).result.sha
|
|
get_parent().print_debug_message("created new tree of files")
|
|
emit_signal("new_tree")
|
|
422:
|
|
get_parent().print_debug_message("could not process the new tree, the file may be corrupted or already present in the repository without any changes.")
|
|
emit_signal("new_tree")
|
|
REQUESTS.NEW_COMMIT:
|
|
match response_code:
|
|
201:
|
|
sha_new_commit = JSON.parse(body.get_string_from_utf8()).result.sha
|
|
get_parent().print_debug_message("created new commit")
|
|
emit_signal("new_commit")
|
|
422:
|
|
get_parent().print_debug_message("could not process the new commit, tree may be invalid.")
|
|
emit_signal("new_commit")
|
|
REQUESTS.PUSH:
|
|
if response_code == 200:
|
|
get_parent().print_debug_message("pushed and committed with success!")
|
|
if not lfs.size():
|
|
get_parent().loading(false)
|
|
Loading.hide()
|
|
emit_signal("pushed")
|
|
REQUESTS.LFS:
|
|
if response_code == 200:
|
|
var object = JSON.parse(body.get_string_from_utf8()).result
|
|
# print(response_code," ",JSON.parse(body.get_string_from_utf8()).result)
|
|
lfs.clear()
|
|
lfs = object.objects as Array
|
|
get_parent().print_debug_message("posted all git lfs files, now uploading...")
|
|
emit_signal("lfs")
|
|
REQUESTS.POST_LFS:
|
|
# if response_code == 200:
|
|
get_parent().print_debug_message(response_code+" "+JSON.parse(body.get_string_from_utf8()).result)
|
|
emit_signal("lfs_push")
|
|
get_parent().loading(false)
|
|
Loading.hide()
|
|
|
|
|
|
func load_branches(br : Array, s_r : Dictionary, ct : Array, gitignore : Dictionary) :
|
|
_branch.clear()
|
|
repo_selected = s_r
|
|
branches_contents = ct
|
|
branches = br
|
|
for branch in branches:
|
|
_branch.add_item(branch.name)
|
|
|
|
gitignore_file = gitignore
|
|
if gitignore:
|
|
Gitignore.set_text(Marshalls.base64_to_utf8(gitignore.content))
|
|
else:
|
|
Gitignore.set_text("")
|
|
|
|
|
|
|
|
repository.set_text(repo_selected.name+"/"+_branch.get_item_text(branch_idx))
|
|
|
|
func selected_branch(id : int):
|
|
branch_idx = id
|
|
repository.set_text(repo_selected.name+"/"+_branch.get_item_text(branch_idx))
|
|
#update_gitignore()
|
|
|
|
# |---------------------------------------------------------|
|
|
|
|
func _on_Button_pressed():
|
|
# Loading.show()
|
|
get_parent().loading(true)
|
|
|
|
load_gitignore()
|
|
get_parent().print_debug_message("fetching all files in project...")
|
|
|
|
|
|
request_sha_latest_commit()
|
|
|
|
#func update_gitignore():
|
|
# var gitignore_filepath = UserData.directory+repo_selected.name+"/"+_branch.get_item_text(branch_idx)+"/"
|
|
# var ignorefile = File.new()
|
|
# var error = ignorefile.open(gitignore_filepath+".gitignore",File.WRITE)
|
|
# if error:
|
|
# Gitignore.set_text(ignorefile.get_as_text())
|
|
# ignorefile.close()
|
|
|
|
# ------- gitignore ----
|
|
func load_gitignore():
|
|
list_file_size.clear()
|
|
|
|
var gitignore_filepath = UserData.directory+repo_selected.name+"/"+_branch.get_item_text(branch_idx)+"/"
|
|
|
|
var dir = Directory.new()
|
|
if not dir.dir_exists(gitignore_filepath):
|
|
dir.open(UserData.directory)
|
|
dir.make_dir_recursive(gitignore_filepath)
|
|
get_parent().print_debug_message("made directory in user folder for this .gitignore file, at %s"%gitignore_filepath)
|
|
|
|
var ignorefile = File.new()
|
|
var error = ignorefile.open(gitignore_filepath+".gitignore",File.WRITE)
|
|
for line in range(0,Gitignore.get_line_count()):
|
|
var gitline = Gitignore.get_line(line)
|
|
ignorefile.store_line(gitline)
|
|
if gitline.begins_with("*"):
|
|
IGNORE_FILES.append(gitline.lstrip("*."))
|
|
elif gitline.ends_with("/"):
|
|
IGNORE_FOLDERS.append(gitline.rstrip("/"))
|
|
elif gitline.begins_with("#") or gitline.begins_with("!"):
|
|
pass
|
|
ignorefile.close()
|
|
|
|
# load the gitignore
|
|
files.push_front(gitignore_filepath+".gitignore")
|
|
|
|
# load the gitattributes
|
|
if File.new().file_exists(UserData.directory+repo_selected.name+"/"+_branch.get_item_text(branch_idx)+"/.gitattributes"):
|
|
files.push_front(UserData.directory+repo_selected.name+"/"+_branch.get_item_text(branch_idx)+"/.gitattributes")
|
|
|
|
lfs.clear()
|
|
|
|
var filtered_files : Array = []
|
|
|
|
for file in files:
|
|
var filter_file = false
|
|
|
|
if file in IGNORE_FILES or file.get_file() in IGNORE_FILES or file.get_extension() in IGNORE_FILES:
|
|
continue
|
|
else:
|
|
for folder in IGNORE_FOLDERS:
|
|
if file.get_base_dir() == folder or folder in file.get_base_dir():
|
|
filter_file = true
|
|
|
|
if not filter_file:
|
|
filtered_files.append(file)
|
|
var size_file = File.new()
|
|
size_file.open(file,File.READ)
|
|
list_file_size.append(size_file.get_len())
|
|
size_file.close()
|
|
|
|
files.clear()
|
|
files = filtered_files
|
|
# files.push_front(gitignore_filepath+".gitignore")
|
|
emit_signal("files_filtered")
|
|
|
|
# |---------------------------------------------------------|
|
|
|
|
func request_sha_latest_commit():
|
|
requesting = REQUESTS.LATEST_COMMIT
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/refs/heads/"+_branch.get_item_text(branch_idx),UserData.header,false,HTTPClient.METHOD_GET,"")
|
|
yield(self,"latest_commit")
|
|
request_base_tree()
|
|
|
|
func request_base_tree():
|
|
requesting = REQUESTS.BASE_TREE
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/commits/"+sha_latest_commit,UserData.header,false,HTTPClient.METHOD_GET,"")
|
|
yield(self,"base_tree")
|
|
request_blobs()
|
|
|
|
func request_blobs():
|
|
requesting = REQUESTS.BLOB
|
|
list_file_sha.clear()
|
|
for file in files:
|
|
current_handled_file = file
|
|
if list_file_size[files.find(file)] < 104857600:
|
|
var content = ""
|
|
var sha = "" # is set to update a file
|
|
var encoding = ""
|
|
|
|
var f : File = File.new()
|
|
f.open(file,File.READ)
|
|
content = Marshalls.raw_to_base64(f.get_buffer(f.get_len()))
|
|
encoding = "base64"
|
|
f.close()
|
|
|
|
get_parent().print_debug_message("blobbing ~> "+file.get_file())
|
|
|
|
var bod = {
|
|
"content":content,
|
|
"encoding":encoding,
|
|
}
|
|
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/blobs",
|
|
UserData.header,false,HTTPClient.METHOD_POST,JSON.print(bod))
|
|
yield(self,"file_blobbed")
|
|
else:
|
|
get_parent().print_debug_message("pointing large file, please wait...")
|
|
var output = []
|
|
OS.execute( 'git', [ "lfs", "pointer",'--file',ProjectSettings.globalize_path(file)], true, output )
|
|
var oid : String = output[0].split(":",false)[2]
|
|
var onlyoid : String = oid.rstrip("size").split(" ")[0].replace("\nsize","")
|
|
list_file_sha.append(onlyoid)
|
|
Progress.set_value(range_lerp(files.find(file),0,files.size(),0,100))
|
|
|
|
get_parent().print_debug_message("blobbed each file with success, start committing...")
|
|
Progress.set_value(100)
|
|
request_commit_tree()
|
|
|
|
func request_commit_tree():
|
|
requesting = REQUESTS.NEW_TREE
|
|
var tree = []
|
|
for file in files:
|
|
if list_file_sha[files.find(file)] != "no-sha":
|
|
pass
|
|
else:
|
|
continue
|
|
var file_name : String = files[files.find(file)].right((DIRECTORY).length())
|
|
if file_name.begins_with("/"):
|
|
file_name = file_name.lstrip("/")
|
|
if list_file_size[files.find(file)] < 104857600:
|
|
if file.get_file() == ".gitignore":
|
|
tree.append({
|
|
"path":".gitignore",
|
|
"mode":"100644",
|
|
"type":"blob",
|
|
"sha":list_file_sha[files.find(file)],
|
|
})
|
|
elif file.get_file() == ".gitattributes":
|
|
tree.append({
|
|
"path":".gitattributes",
|
|
"mode":"100644",
|
|
"type":"blob",
|
|
"sha":list_file_sha[files.find(file)],
|
|
})
|
|
else:
|
|
tree.append({
|
|
"path":file_name,
|
|
"mode":"100644",
|
|
"type":"blob",
|
|
"sha":list_file_sha[files.find(file)],
|
|
})
|
|
|
|
else:
|
|
lfs.append({"oid": list_file_sha[files.find(file)],"size": list_file_size[files.find(file)]})
|
|
|
|
var bod : Dictionary = {
|
|
"base_tree": sha_base_tree,
|
|
"tree":tree
|
|
}
|
|
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/trees",UserData.header,false,HTTPClient.METHOD_POST,JSON.print(bod))
|
|
yield(self,"new_tree")
|
|
request_new_commit()
|
|
|
|
func request_new_commit():
|
|
requesting = REQUESTS.NEW_COMMIT
|
|
var message = _message.text
|
|
var bod = {
|
|
"parents": [sha_latest_commit],
|
|
"tree": sha_new_tree,
|
|
"message": message
|
|
}
|
|
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/commits",UserData.header,false,HTTPClient.METHOD_POST,JSON.print(bod))
|
|
yield(self,"new_commit")
|
|
request_push_commit()
|
|
|
|
func request_push_commit():
|
|
requesting = REQUESTS.PUSH
|
|
var bod = {
|
|
"sha": sha_new_commit
|
|
}
|
|
new_repo.request("https://api.github.com/repos/"+repo_selected.owner.login+"/"+repo_selected.name+"/git/refs/heads/"+_branch.get_item_text(branch_idx),UserData.header,false,HTTPClient.METHOD_POST,JSON.print(bod))
|
|
yield(self,"pushed")
|
|
|
|
if lfs.size() > 0:
|
|
requesting = REQUESTS.LFS
|
|
var body = {"operation": "upload","ref": {"name":"refs/heads/"+_branch.get_item_text(branch_idx)},"transfers": [ "basic" ],"objects": lfs}
|
|
new_repo.request("https://github.com/"+repo_selected.owner.login+"/"+repo_selected.name+UserData.gitlfs_request,UserData.gitlfs_header,false,HTTPClient.METHOD_POST,JSON.print(body))
|
|
yield(self,"lfs")
|
|
|
|
if lfs.size() > 0:
|
|
requesting = REQUESTS.POST_LFS
|
|
get_parent().print_debug_message(lfs)
|
|
var body = { "transfer":"basic" , "objects":lfs}
|
|
new_repo.request("https://github.com/"+repo_selected.owner.login+"/"+repo_selected.name+UserData.gitlfs_request,UserData.gitlfs_header,false,HTTPClient.METHOD_PUT,JSON.print(body))
|
|
yield(self,"lfs_push")
|
|
|
|
empty_fileds()
|
|
Progress.set_value(0)
|
|
get_parent().Repo._on_reload_pressed()
|
|
|
|
# --------------------------------------@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
|
|
|
func _on_loading2_visibility_changed():
|
|
var Mat = Loading.get_material()
|
|
if Loading.visible:
|
|
Mat.set_shader_param("speed",5)
|
|
else:
|
|
Mat.set_shader_param("speed",0)
|
|
|
|
func _on_close2_pressed():
|
|
empty_fileds()
|
|
hide()
|
|
get_parent().Repo.show()
|
|
|
|
func empty_fileds():
|
|
files.clear()
|
|
directories.clear()
|
|
sha_latest_commit = ""
|
|
sha_base_tree = ""
|
|
sha_new_tree = ""
|
|
sha_new_commit = ""
|
|
list_file_sha.clear()
|
|
IGNORE_FILES.resize(0)
|
|
IGNORE_FOLDERS.resize(0)
|
|
_message.text = ""
|
|
|
|
Uncommitted.clear()
|
|
|
|
# hide()
|
|
# get_parent().Repo.show()
|
|
|
|
func on_gitignore_toggled(toggle : bool):
|
|
Gitignore.set_readonly(!toggle)
|
|
|
|
func on_confirm():
|
|
pass
|
|
|
|
func on_files_selected(paths : PoolStringArray):
|
|
for path in paths:
|
|
if not files.has(path):
|
|
files.append(path)
|
|
else:
|
|
files.erase(path)
|
|
|
|
show_files(paths,true,false)
|
|
|
|
func on_dir_selected(path : String):
|
|
var directories = []
|
|
var dir = Directory.new()
|
|
dir.open(path)
|
|
dir.list_dir_begin(true,false)
|
|
var file = dir.get_next()
|
|
while (file != ""):
|
|
if dir.current_is_dir():
|
|
var directorypath = dir.get_current_dir()+"/"+file
|
|
directories.append(directorypath)
|
|
else:
|
|
var filepath = dir.get_current_dir()+"/"+file
|
|
if not files.has(filepath):
|
|
files.append(filepath)
|
|
|
|
file = dir.get_next()
|
|
|
|
dir.list_dir_end()
|
|
|
|
show_files(files,true,false)
|
|
|
|
for directory in directories:
|
|
on_dir_selected(directory)
|
|
|
|
func show_files(paths : PoolStringArray, isfile : bool = false , isdir : bool = false):
|
|
Uncommitted.clear()
|
|
|
|
for file in paths:
|
|
file = file.replace("///","//")
|
|
if isfile:
|
|
Uncommitted.add_item(file,IconLoaderGithub.load_icon_from_name("file-gray"))
|
|
|
|
# if isdir:
|
|
# for dir in paths:
|
|
# Uncommitted.add_item(dir,IconLoaderGithub.load_icon_from_name("dir"))
|
|
|
|
func on_removefile_pressed():
|
|
var filestoremove = Uncommitted.get_selected_items()
|
|
if filestoremove.size() == 0:
|
|
on_nothing_selected()
|
|
return
|
|
var first_file = filestoremove[0]
|
|
var file_name = Uncommitted.get_item_text(first_file)
|
|
files.erase(file_name)
|
|
Uncommitted.remove_item(first_file)
|
|
if Uncommitted.get_selected_items().size() > 0:
|
|
on_removefile_pressed()
|
|
else:
|
|
on_nothing_selected()
|
|
|
|
func on_selectfiles_pressed():
|
|
SelectFiles.set_mode(FileDialog.MODE_OPEN_FILES)
|
|
SelectFiles.invalidate()
|
|
SelectFiles.popup()
|
|
|
|
func on_selectdirectory_pressed():
|
|
SelectFiles.set_mode(FileDialog.MODE_OPEN_DIR)
|
|
SelectFiles.invalidate()
|
|
SelectFiles.popup()
|
|
|
|
func on_item_selected(idx : int):
|
|
removefileBtn.set_disabled(false)
|
|
|
|
func on_multiple_item_selected(idx : int, selected : bool):
|
|
removefileBtn.set_disabled(false)
|
|
|
|
func on_nothing_selected():
|
|
removefileBtn.set_disabled(true)
|
|
|
|
func about_gitignore_pressed():
|
|
OS.shell_open("https://git-scm.com/docs/gitignore")
|
|
|