-
Notifications
You must be signed in to change notification settings - Fork 11
/
gitignore_plugin.py
228 lines (187 loc) · 7.56 KB
/
gitignore_plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import sublime
import subprocess
import os
import os.path
import threading
import platform
from time import sleep
# Used for output suppression when calling subprocess functions; see
# http://stackoverflow.com/questions/10251391/suppressing-output-in-python-subprocess-call
devnull = open(os.devnull, 'w')
# Used to prevent a new command prompt window from popping up every time a new
# process is spawned on Windows. See
# https://docs.python.org/2/library/subprocess.html#subprocess.STARTUPINFO
if platform.system() == 'Windows':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
else:
startupinfo = None
def start(): # Gets invoked at the bottom of this file.
"""
Regularly (every 5s) updates the file_exclude_patterns setting.
"""
if is_first_launch():
migrate_exclude_patterns()
record_first_launch()
def run():
update_file_exclude_patterns()
sublime.set_timeout(run, 5000)
run()
def update_file_exclude_patterns():
"""
Updates the "file_exclude_patterns" preference to include all .gitignored
files.
Also includes any additional files or folders listed in the
"extra_file_exclude_patterns" and "extra_folder_exclude_patterns" settings.
"""
s = sublime.load_settings("Preferences.sublime-settings")
file_exclude_patterns = s.get('extra_file_exclude_patterns', []) or []
folder_exclude_patterns = s.get('extra_folder_exclude_patterns', []) or []
for path in all_ignored_paths():
is_directory = os.path.isdir(path)
if platform.system() == 'Windows':
# For some bizarre reason Sublime wants all its filenames to look like
# C/somedir/somefile
# instead of
# C:\somedir\somefile
# as they are normally written on Windows, and will not understand the
# latter at all. All the other functions in this file return paths with
# OS-standard separtors and include the colon after the drive letter on
# Windows, so we need to convert them here to Sublime-format.
path = windows_path_to_sublime_path(path)
if is_directory:
folder_exclude_patterns.append(path)
else:
file_exclude_patterns.append(path)
new_files = set(file_exclude_patterns)
old_files = set(s.get('file_exclude_patterns', []) or [])
new_folders = set(folder_exclude_patterns)
old_folders = set(s.get('folder_exclude_patterns', []) or [])
# Only make changes if anything has actually changed, to avoid spamming the
# sublime console
if new_files != old_files or new_folders != old_folders:
s.set('file_exclude_patterns', list(file_exclude_patterns))
s.set('folder_exclude_patterns', list(folder_exclude_patterns))
sublime.save_settings("Preferences.sublime-settings")
def all_ignored_paths():
"""
Returns a list of all .gitignored files or folders contained in repos
contained within or containing any folders open in any open windows.
"""
open_folders = set()
for window in sublime.windows():
open_folders.update(window.folders())
all_ignored_paths = set()
for folder in open_folders:
ignored_paths = folder_ignored_paths(folder)
all_ignored_paths.update(ignored_paths)
return list(all_ignored_paths)
def folder_ignored_paths(folder):
"""
Returns a list (without duplicates, in the case of weird repo-nesting) of
all files/folders within the given folder that are git ignored.
The folder itself need not be the top level of a git repo, nor even within
a git repo.
"""
all_ignored_paths = set()
# First find all repos CONTAINED in this folder:
repos = set(find_git_repos(folder))
# Then, additionally, if this folder is itself contained within a repo,
# find the .git folder of the repo containing it:
if is_in_git_repo(folder):
repos.add(parent_repo_path(folder))
# Now we find all the ignored paths in any of the above repos
for git_repo in repos:
ignored_paths = repo_ignored_paths(git_repo)
all_ignored_paths.update(ignored_paths)
return list(all_ignored_paths)
def is_in_git_repo(folder):
"""
Returns true if the given folder is contained within a git repo
"""
exit_code = subprocess.call(
["git", "rev-parse", "--is-inside-work-tree"],
cwd=folder,
stdout=devnull,
stderr=devnull,
startupinfo=startupinfo
)
return exit_code == 0
def parent_repo_path(folder):
"""
Takes the path to a folder contained within a git repo, and returns the
parent repo.
"""
# abspath call converts forward slashes to backslashes on Windows; we do
# this wherever necessary to keep the format of our paths standardised on
# Windows.
return os.path.abspath(
subprocess.Popen(
['git', 'rev-parse', '--show-toplevel'],
stdout=subprocess.PIPE,
cwd=folder,
startupinfo=startupinfo
).stdout.read().decode('utf-8', 'ignore').strip()
)
def find_git_repos(folder):
"""
Returns a list of all git repos within the given ancestor folder.
"""
return [root for root, subfolders, files
in os.walk(folder)
if '.git' in subfolders]
def repo_ignored_paths(git_repo):
"""
Takes the path of a git repo and lists all ignored files/folders in the
repo.
"""
# Trick for listing ignored files nicked from
# http://stackoverflow.com/a/2196755/1709587
command_output = subprocess.Popen(
['git', 'clean', '-ndX'],
stdout=subprocess.PIPE,
cwd=git_repo,
startupinfo=startupinfo,
env={'LANG':'C'}
).stdout.read()
command_output = command_output.decode('utf-8', 'ignore')
if command_output.isspace() or command_output == u'':
return []
lines = command_output.strip().split(u'\n')
# Each line in `lines` now looks something like:
# "Would remove foo/bar/yourfile.txt"
relative_paths = [line.replace(u'Would remove ', u'', 1).rstrip(u'/')
for line in lines]
absolute_paths = [os.path.join(git_repo, path) for path in relative_paths]
return absolute_paths
def is_first_launch():
s = sublime.load_settings("gitignorer.sublime-settings")
return not s.get('_sublime_gitignorer_has_run', False)
def migrate_exclude_patterns():
"""
Runs on first launch; purpose is to prevent people who have already set
exclusion patterns from losing them when they install this package.
"""
s = sublime.load_settings("Preferences.sublime-settings")
existing_file_exclude_patterns = s.get('file_exclude_patterns', []) or []
existing_folder_exclude_patterns = s.get('folder_exclude_patterns', []) or []
s.set('extra_file_exclude_patterns', existing_file_exclude_patterns)
s.set('extra_folder_exclude_patterns', existing_folder_exclude_patterns)
sublime.save_settings("Preferences.sublime-settings")
def record_first_launch():
s = sublime.load_settings("gitignorer.sublime-settings")
s.set('_sublime_gitignorer_has_run', True)
sublime.save_settings("gitignorer.sublime-settings")
def windows_path_to_sublime_path(path):
"""
Removes the colon after the drive letter and replaces backslashes with
slashes.
e.g.
windows_path_to_sublime_path("C:\somedir\somefile")
== "C/somedir/somefile"
"""
assert(path[1] == u':')
without_colon = path[0] + path[2:]
return without_colon.replace(u'\\', u'/')
start()