ghostnotes 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ghostnotes/__init__.py ADDED
File without changes
ghostnotes/cli.py ADDED
@@ -0,0 +1,54 @@
1
+ # CLI setup
2
+ import argparse
3
+ from ghostnotes.hook import install_hook, strip_ghostnotes
4
+ from ghostnotes.config import create_config, update_exclude, set_tag, add_lang_support
5
+ from ghostnotes.sync import pull, extract_notes
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="GhostNotes")
9
+ subparsers = parser.add_subparsers(dest="command")
10
+
11
+ # init
12
+ init_parser = subparsers.add_parser("init", help="Initialize GhostNotes")
13
+
14
+ # set tag
15
+ tag_parser = subparsers.add_parser("set-tag", help="Set the tag for GhostNotes to ignore, Default tag is 'GN:'. ")
16
+ tag_parser.add_argument("--tag", required=True, help="New tag")
17
+
18
+ # add lang
19
+ lang_parser = subparsers.add_parser("add-lang", help="Add a new language support, Check .ghostnotes for the default languages supported")
20
+ lang_parser.add_argument("--ext", required=True, help="File extension (.py, .js, .sql etc.)")
21
+ lang_parser.add_argument("--symb", required=True, help="Symbol for commenting in the said language (# for .py, // for .java etc.)")
22
+
23
+ # pull
24
+ pull_parser = subparsers.add_parser("pull", help="Git pull with ghostnote-safe stripping and re-applying")
25
+
26
+ # status
27
+ status_parser = subparsers.add_parser("status", help="Show all ghost notes in the project")
28
+
29
+ args = parser.parse_args()
30
+
31
+ if args.command == "init":
32
+ create_config()
33
+ update_exclude()
34
+ install_hook()
35
+ print("GhostNotes is successfully initialized.")
36
+ elif args.command == "set-tag":
37
+ set_tag(args.tag)
38
+ elif args.command == "add-lang":
39
+ add_lang_support(args.ext, args.symb)
40
+ elif args.command == "pull":
41
+ pull()
42
+ elif args.command == "status":
43
+ notes = extract_notes()
44
+ if not notes:
45
+ print("No ghost notes found.")
46
+ else:
47
+ total = sum(len(v) for v in notes.values())
48
+ print(f"Found {total} ghost note(s):\n")
49
+ for file, file_notes in notes.items():
50
+ for n in file_notes:
51
+ print(f" {file}:{n['line_number'] + 1} — {n['note']}")
52
+
53
+ if __name__ == "__main__":
54
+ main()
ghostnotes/config.py ADDED
@@ -0,0 +1,74 @@
1
+ # file for creating the .ghostnotes configuration
2
+ import configparser
3
+ import os
4
+
5
+ def create_config():
6
+ if not os.path.isdir('.git'):
7
+ print("This project is not initialized with git. GhostNotes only works for directories already initialized with git.")
8
+ return
9
+
10
+ else:
11
+ # set-up the default config
12
+ config = configparser.ConfigParser()
13
+ config['settings'] = {'tag': 'GN:'}
14
+ config['languages'] = {'.py': '#', '.java': '//', '.js': '//', '.ts': '//'}
15
+
16
+ with open('.ghostnotes', 'w') as configfile:
17
+ config.write(configfile)
18
+
19
+ print("GhostNotes: Config file is created succesfully")
20
+
21
+ def update_exclude():
22
+ loc = ".git/info/exclude"
23
+
24
+ with open(loc, 'r') as file:
25
+ for line in file:
26
+ # already in exclude
27
+ if line.strip() == '.ghostnotes':
28
+ return
29
+
30
+ file = open(loc, "a")
31
+ file.write('.ghostnotes\n')
32
+ file.close()
33
+
34
+ def load_config():
35
+ # check if config exists
36
+ if not os.path.isfile('.ghostnotes'):
37
+ print("This project is not initialized with GhostNotes.")
38
+ return
39
+
40
+ else:
41
+ # set-up the default config
42
+ config = configparser.ConfigParser()
43
+ config.read('.ghostnotes')
44
+ print("GhostNotes: Config file is read succesfully")
45
+
46
+ return config
47
+
48
+ # update tag
49
+ def set_tag(tag):
50
+ if not os.path.isfile('.ghostnotes'):
51
+ print("This project is not initialized with GhostNotes, cannot set new tag.")
52
+ return
53
+
54
+ config = configparser.ConfigParser()
55
+ config.read('.ghostnotes') # preserve existing settings
56
+ config['settings']['tag'] = tag
57
+ with open('.ghostnotes', 'w') as configfile:
58
+ config.write(configfile)
59
+
60
+ print(f"GhostNotes: Config file tag is set to {tag} succesfully")
61
+
62
+ # add custom language comment support
63
+ def add_lang_support(extension, notation):
64
+ if not os.path.isfile('.ghostnotes'):
65
+ print("This project is not initialized with GhostNotes, cannot add new file support.")
66
+ return
67
+
68
+ config = configparser.ConfigParser()
69
+ config.read('.ghostnotes')
70
+ config['languages'][extension] = notation
71
+ with open('.ghostnotes', 'w') as configfile:
72
+ config.write(configfile)
73
+
74
+ print(f"GhostNotes: Config file language support for {extension} files are added with symbol {notation} successfuly.")
ghostnotes/hook.py ADDED
@@ -0,0 +1,103 @@
1
+ import os
2
+ import stat
3
+ import subprocess
4
+ from pathlib import Path
5
+ from ghostnotes.config import load_config
6
+
7
+ # for adding this script to pre-commit
8
+ def install_hook():
9
+ path = '.git/hooks/pre-commit'
10
+ command = 'python3 -m ghostnotes.hook'
11
+
12
+ # if a pre-commit hook file already exists:
13
+ if os.path.exists(path):
14
+ with open(path, 'r') as file:
15
+ for line in file:
16
+ if line.strip() == command:
17
+ return
18
+
19
+ with open(path, 'a') as file:
20
+ file.write(f'\n{command}\n')
21
+ else:
22
+ with open(path, 'w') as file:
23
+ file.write(f'#!/bin/bash\n{command}\n')
24
+
25
+ # make the file executable
26
+ os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC)
27
+
28
+ # main logic function for stripping the committed files of the keyworded comments
29
+ def strip_ghostnotes():
30
+ """
31
+ Read Config
32
+ Get the list of staged files — how do you ask git which files are staged? Think about what terminal command you'd run manually.
33
+ Filter to only supported extensions
34
+ For each supported file, strip lines matching the tag.
35
+ Re-stage the files with git add
36
+ """
37
+ # read config
38
+ configuration = load_config()
39
+
40
+ if configuration is None:
41
+ print("Ghostnotes is not configured.")
42
+ return
43
+
44
+ # get the list of staged file:
45
+ res = subprocess.run(['git', 'diff', '--name-only', '--cached'], capture_output=True, text=True)
46
+ staged_files = res.stdout.splitlines()
47
+ supported_staged_files = list()
48
+
49
+ # filter staged_files to only supported file types
50
+ for f in staged_files:
51
+ if Path(f).suffix in configuration['languages']:
52
+ supported_staged_files.append(f)
53
+
54
+ # strip lines matching the tag
55
+ for file in supported_staged_files:
56
+ # check the commenting style for the file type from configuration['languages'] as well as the tag from configuration['settings'] to build the exact sequence of chars we are looking for
57
+ comment = configuration['languages'][Path(file).suffix]
58
+ tag = configuration['settings']['tag']
59
+ pattern = comment + ' ' + tag
60
+
61
+ # read the staged version from the git index, not the working tree
62
+ staged_content = subprocess.run(
63
+ ['git', 'show', f':{file}'],
64
+ capture_output=True, text=True
65
+ ).stdout
66
+
67
+ new_lines = list()
68
+ for line in staged_content.splitlines(keepends=True):
69
+ # strip only if the pattern is not inside a string literal
70
+ if pattern in line:
71
+ idx = line.index(pattern)
72
+ prefix = line[:idx]
73
+ # if an odd number of quotes precede the pattern, it's inside a string
74
+ in_string = (prefix.count('"') % 2 == 1) or (prefix.count("'") % 2 == 1)
75
+ if not in_string:
76
+ line = line[:idx].rstrip() + '\n'
77
+ new_lines.append(line)
78
+
79
+ stripped_content = ''.join(new_lines)
80
+
81
+ # write the stripped content back into the git index only
82
+ hash_result = subprocess.run(
83
+ ['git', 'hash-object', '-w', '--stdin'],
84
+ input=stripped_content, capture_output=True, text=True
85
+ )
86
+ blob_hash = hash_result.stdout.strip()
87
+
88
+ # get the file's mode from the index
89
+ ls_result = subprocess.run(
90
+ ['git', 'ls-files', '--stage', file],
91
+ capture_output=True, text=True
92
+ )
93
+ if not ls_result.stdout.strip():
94
+ continue
95
+ mode = ls_result.stdout.split()[0]
96
+
97
+ # update the index entry with the new blob
98
+ subprocess.run(
99
+ ['git', 'update-index', '--cacheinfo', f'{mode},{blob_hash},{file}']
100
+ )
101
+
102
+ if __name__ == "__main__":
103
+ strip_ghostnotes()
ghostnotes/sync.py ADDED
@@ -0,0 +1,140 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from ghostnotes.config import load_config
4
+
5
+
6
+ def extract_notes():
7
+ config = load_config()
8
+ notes = {}
9
+
10
+ for file in Path('.').rglob('*'):
11
+ # skip hidden directories (.git, .venv, etc.)
12
+ if any(part.startswith('.') for part in file.parts):
13
+ continue
14
+
15
+ if not file.is_file():
16
+ continue
17
+
18
+ ext = file.suffix
19
+ if ext not in config['languages']:
20
+ continue
21
+
22
+ comment = config['languages'][ext]
23
+ tag = config['settings']['tag']
24
+ pattern = comment + ' ' + tag
25
+
26
+ # extract notes, store code without comment, store comment, and store line num
27
+ file_notes = []
28
+ try:
29
+ with open(file, 'r') as f:
30
+ for i, line in enumerate(f):
31
+ if pattern in line:
32
+ stripped = line.split(pattern)[0].rstrip()
33
+ note_text = line.split(pattern, 1)[1].strip()
34
+ file_notes.append({
35
+ 'stripped_line': stripped,
36
+ 'note': note_text,
37
+ 'line_number': i,
38
+ })
39
+ except UnicodeDecodeError:
40
+ continue
41
+
42
+ if file_notes:
43
+ notes[str(file)] = file_notes
44
+
45
+ return notes
46
+
47
+ def strip_working_tree(notes):
48
+ for file, file_notes in notes.items():
49
+ patterns = {n['line_number'] for n in file_notes}
50
+
51
+ with open(file, 'r') as f:
52
+ lines = f.readlines()
53
+
54
+ for line_num in patterns:
55
+ note_entry = next(n for n in file_notes if n['line_number'] == line_num)
56
+ lines[line_num] = note_entry['stripped_line'] + '\n'
57
+
58
+ with open(file, 'w') as f:
59
+ f.writelines(lines)
60
+
61
+
62
+ def reapply_notes(notes, config):
63
+ for file, file_notes in notes.items():
64
+ if not Path(file).is_file():
65
+ for n in file_notes:
66
+ print(f" ORPHANED (file deleted): {file} — {n['note']}")
67
+ continue
68
+
69
+ comment = config['languages'][Path(file).suffix]
70
+ tag = config['settings']['tag']
71
+ pattern = comment + ' ' + tag
72
+
73
+ with open(file, 'r') as f:
74
+ lines = f.readlines()
75
+
76
+ orphaned = []
77
+
78
+ for n in file_notes:
79
+ target = n['stripped_line']
80
+ line_num = n['line_number']
81
+ matched = False
82
+
83
+ # 1. exact match at same line number
84
+ if line_num < len(lines) and lines[line_num].rstrip() == target:
85
+ lines[line_num] = target + ' ' + pattern + ' ' + n['note'] + '\n'
86
+ matched = True
87
+
88
+ # 2. exact match nearby (within 20 lines)
89
+ if not matched:
90
+ start = max(0, line_num - 20)
91
+ end = min(len(lines), line_num + 20)
92
+ for i in range(start, end):
93
+ if lines[i].rstrip() == target:
94
+ lines[i] = target + ' ' + pattern + ' ' + n['note'] + '\n'
95
+ matched = True
96
+ break
97
+
98
+ # 3. exact match anywhere in file
99
+ if not matched:
100
+ for i in range(len(lines)):
101
+ if lines[i].rstrip() == target:
102
+ lines[i] = target + ' ' + pattern + ' ' + n['note'] + '\n'
103
+ matched = True
104
+ break
105
+
106
+ # 4. orphaned
107
+ if not matched:
108
+ orphaned.append(n)
109
+
110
+ with open(file, 'w') as f:
111
+ f.writelines(lines)
112
+
113
+ for n in orphaned:
114
+ print(f" ORPHANED: {file}:{n['line_number']} — {n['note']}")
115
+
116
+
117
+ def pull():
118
+ config = load_config()
119
+ notes = extract_notes()
120
+
121
+ if notes:
122
+ print(f"GhostNotes: Saved {sum(len(v) for v in notes.values())} note(s), stripping before pull...")
123
+ strip_working_tree(notes)
124
+
125
+ result = subprocess.run(['git', 'pull'], capture_output=True, text=True)
126
+ print(result.stdout)
127
+ if result.stderr:
128
+ print(result.stderr)
129
+
130
+ if notes:
131
+ if result.returncode != 0:
132
+ print("GhostNotes: Pull failed, restoring notes to working tree...")
133
+ reapply_notes(notes, config)
134
+ print("GhostNotes: Notes restored. Resolve the pull issue and try again.")
135
+ else:
136
+ print("GhostNotes: Re-applying notes...")
137
+ reapply_notes(notes, config)
138
+ print("GhostNotes: Done.")
139
+
140
+ return result.returncode
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostnotes
3
+ Version: 1.0.0
4
+ Summary: Strip personal annotations from code before committing
5
+ Author-email: Tuna Akin <tuna.takin@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # GhostNotes
13
+ **Version:** 1.0.0 | **Last Updated:** April 12, 2026
14
+
15
+ A Git tool that strips tagged comments before committing.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install ghostnotes
23
+ ```
24
+
25
+ Then initialize in any git repo:
26
+
27
+ ```bash
28
+ cd your-project
29
+ ghostnotes init
30
+ ```
31
+
32
+ ---
33
+
34
+ ## What it does
35
+ GhostNotes lets you leave personal notes in your code using a tag (default: `GN:`). Before every commit, it automatically strips them out so they never reach your repository.
36
+ ```python
37
+ x = some_function() # GN: this breaks when input is negative, fix later
38
+ ```
39
+ This comment line gets stripped before the commit so it looks like:
40
+ ```python
41
+ x = some_function()
42
+ ```
43
+
44
+ Your local file stays untouched.
45
+
46
+ ---
47
+
48
+ ## Commands
49
+
50
+ | Command | Description |
51
+ |---|---|
52
+ | `ghostnotes init` | Initialize GhostNotes in the current project |
53
+ | `ghostnotes status` | Show all GhostNotes in the current project |
54
+ | `ghostnotes set-tag --tag <tag>` | Change the default comment tag |
55
+ | `ghostnotes add-lang --ext <.ext> --symb <symbol>` | Add support for a new language |
56
+ | `ghostnotes pull` | Git pull that safely strips and re-applies your ghostnotes |
@@ -0,0 +1,11 @@
1
+ ghostnotes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ghostnotes/cli.py,sha256=mSpnu9y1oLkShcbH6Z1IEh0XAr6n3nC6CQ1KS82LvGQ,2116
3
+ ghostnotes/config.py,sha256=CYBoSVkcTn_-IZQuhjlY4HqqIRLrvNlr2_nLl637bRk,2398
4
+ ghostnotes/hook.py,sha256=PnhyxuAZJXXG_uJRl7A59K6yAQHan7dWgmkhgyoZ7-E,3740
5
+ ghostnotes/sync.py,sha256=WpAdVJmH5d-xHS7Luov-mijwByS3I_OaFq3qXvRKptE,4477
6
+ ghostnotes-1.0.0.dist-info/licenses/LICENSE,sha256=nOQImqRAvHs2zvX_672bmFNV8j1E34KEVWXKMLc_YB8,1066
7
+ ghostnotes-1.0.0.dist-info/METADATA,sha256=oKOtOzVMYZhiJkq0-Eu9cVwoi-wuSX92C9gsH9SKt5E,1413
8
+ ghostnotes-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ ghostnotes-1.0.0.dist-info/entry_points.txt,sha256=ePr6r5ZXsebGdIkj_q_hcD8_qkWkiir-PTYCdfHBolc,51
10
+ ghostnotes-1.0.0.dist-info/top_level.txt,sha256=KgbY9qlqn3rtYmhkjV4cnWFn1qReukDtWfyUh8Tbzxg,11
11
+ ghostnotes-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ghostnotes = ghostnotes.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tuna Akin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ghostnotes