git-nearest 0.1.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.
@@ -0,0 +1,3 @@
1
+ """Find the remote branches closest to HEAD."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
git_nearest/cli.py ADDED
@@ -0,0 +1,64 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ from . import __version__, color, graph, refs, status, table
6
+ from .color import Painter
7
+ from .gitutil import GitError, git
8
+
9
+
10
+ def _parser():
11
+ p = argparse.ArgumentParser(
12
+ prog="git nearest",
13
+ description="List the remote branches closest to HEAD (fewest commits ahead, then fewest behind).",
14
+ )
15
+ p.add_argument("limit", nargs="?", type=int, default=5, help="how many refs to show (default: 5)")
16
+ p.add_argument("-g", "--graph", action="store_true", help="draw how HEAD and those refs relate")
17
+ p.add_argument("--color", choices=["auto", "always", "never"], default="auto")
18
+ p.add_argument("--no-color", dest="color", action="store_const", const="never", help="same as --color=never")
19
+ p.add_argument("--no-status", action="store_true", help="don't check the working tree (faster on huge repos)")
20
+ p.add_argument("--version", action="version", version="git-nearest " + __version__)
21
+ return p
22
+
23
+
24
+ def head_line(note, paint):
25
+ """The line above the table: where HEAD is and whether the working tree is clean."""
26
+ if note is None:
27
+ return None
28
+ branch = git("symbolic-ref", "--short", "-q", "HEAD", check=False)
29
+ label = "HEAD -> " + branch if branch else "HEAD (detached)"
30
+ return "%s %s %s" % (paint(color.GREEN_BOLD, "@"), paint(color.GREEN_BOLD, label), note)
31
+
32
+
33
+ def _use_color(mode):
34
+ if mode == "always":
35
+ return True
36
+ return mode == "auto" and sys.stdout.isatty() and not os.environ.get("NO_COLOR")
37
+
38
+
39
+ def main(argv=None):
40
+ args = _parser().parse_args(argv)
41
+ if args.limit < 1:
42
+ sys.exit("git-nearest: limit must be at least 1")
43
+ if os.name == "nt":
44
+ os.system("") # turns on ANSI escape handling in the legacy Windows console
45
+ if not sys.stdout.isatty() and hasattr(sys.stdout, "reconfigure"):
46
+ sys.stdout.reconfigure(encoding="utf-8")
47
+
48
+ try:
49
+ git("rev-parse", "--verify", "-q", "HEAD")
50
+ rows = refs.rank(args.limit)
51
+ if not rows:
52
+ print("git-nearest: no remote branches found (try `git fetch`)", file=sys.stderr)
53
+ return 1
54
+ paint = Painter(_use_color(args.color))
55
+ note = None if args.no_status else status.describe(status.read(), paint)
56
+ if args.graph:
57
+ lines = graph.render([r.ref for r in rows], paint, note)
58
+ else:
59
+ lines = table.render(rows, paint, head_line(note, paint))
60
+ except GitError as e:
61
+ print("git-nearest: " + str(e), file=sys.stderr)
62
+ return 1
63
+ print("\n".join(lines))
64
+ return 0
git_nearest/color.py ADDED
@@ -0,0 +1,18 @@
1
+ """ANSI styling that can be switched off."""
2
+
3
+ DIM = "2"
4
+ GREEN = "32"
5
+ GREEN_BOLD = "1;32"
6
+ RED = "31"
7
+ YELLOW = "33"
8
+ CYAN = "36"
9
+
10
+
11
+ class Painter:
12
+ def __init__(self, enabled):
13
+ self.enabled = enabled
14
+
15
+ def __call__(self, code, text):
16
+ if not self.enabled or not text:
17
+ return text
18
+ return "\033[%sm%s\033[0m" % (code, text)
git_nearest/gitutil.py ADDED
@@ -0,0 +1,24 @@
1
+ """Thin wrapper around the git executable."""
2
+ import subprocess
3
+
4
+
5
+ class GitError(Exception):
6
+ pass
7
+
8
+
9
+ def git(*args, check=True, strip=True):
10
+ """Run git and return stdout (stripped unless strip is False).
11
+
12
+ On failure raise GitError, or return "" when check is False.
13
+ """
14
+ try:
15
+ p = subprocess.run(
16
+ ["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace"
17
+ )
18
+ except FileNotFoundError:
19
+ raise GitError("git executable not found")
20
+ if p.returncode != 0:
21
+ if check:
22
+ raise GitError(p.stderr.strip() or "git " + " ".join(args) + " failed")
23
+ return ""
24
+ return p.stdout.strip() if strip else p.stdout
git_nearest/graph.py ADDED
@@ -0,0 +1,155 @@
1
+ """`jj log`-style graph (newest on top) of HEAD and the given remote refs.
2
+
3
+ Only the commits that matter are drawn: HEAD, the ref tips and the merge-bases between them.
4
+ Everything in between is folded into a "~ (n)" row on the vertical line.
5
+ """
6
+ from . import color
7
+ from .gitutil import git
8
+
9
+ LINE_GLYPHS = set("│├┴┼╯╮┤─~") # │ ├ ┴ ┼ ╯ ╮ ┤ ─ ~
10
+ LEGEND = "~ (n) = n commits folded between"
11
+
12
+
13
+ def render(refs, paint, head_note=None):
14
+ head = git("rev-parse", "HEAD")
15
+ branch = git("symbolic-ref", "--short", "-q", "HEAD", check=False)
16
+ names = {} # commit -> remote refs pointing at it
17
+ shas = git("rev-parse", *["refs/remotes/" + r for r in refs]).split()
18
+ for ref, sha in zip(refs, shas):
19
+ names.setdefault(sha, []).append(ref)
20
+ tips = sorted(set(names) | {head})
21
+ tip_bit = {c: 1 << i for i, c in enumerate(tips)}
22
+
23
+ # Load the history above the common base: children come before parents in `order`.
24
+ base = git("merge-base", "--octopus", *tips, check=False)
25
+ rev = ["rev-list", "--topo-order", "--parents", *tips] + (["^" + base] if base else [])
26
+ parents = {}
27
+ order = []
28
+ for line in git(*rev).splitlines():
29
+ commit, *ps = line.split()
30
+ parents[commit] = ps
31
+ order.append(commit)
32
+ if base:
33
+ parents[base] = []
34
+ order.append(base)
35
+ for commit in order:
36
+ parents[commit] = [p for p in parents[commit] if p in parents]
37
+
38
+ # mask[c] = which tips can reach c. Commits sharing a mask form one region; the newest commit
39
+ # of each region (no child with the same mask) is where branches split: a merge-base.
40
+ mask = {c: tip_bit.get(c, 0) for c in order}
41
+ for c in order:
42
+ for p in parents[c]:
43
+ mask[p] |= mask[c]
44
+ has_twin_child = {p for c in order for p in parents[c] if mask[p] == mask[c]}
45
+ nodes = [c for c in order if c in tip_bit or c not in has_twin_child]
46
+ node_bit = {c: 1 << i for i, c in enumerate(nodes)}
47
+
48
+ # allanc[c] = bitmask of every node below c; the nearest are those no other ancestor node covers.
49
+ allanc = {}
50
+ for c in reversed(order):
51
+ a = 0
52
+ for p in parents[c]:
53
+ a |= allanc[p] | node_bit.get(p, 0)
54
+ allanc[c] = a
55
+
56
+ def on_head_line(c):
57
+ return c == head or bool(allanc[head] & node_bit[c]) or bool(allanc[c] & node_bit[head])
58
+
59
+ def sort_key(c):
60
+ return (not on_head_line(c), sorted(names.get(c, [c]))[0])
61
+
62
+ above = {} # node -> nearest ancestor nodes, head line first
63
+ folded = {} # node -> commits between it and those ancestors
64
+ for c in nodes:
65
+ a = allanc[c]
66
+ covered = 0
67
+ for n in nodes:
68
+ if a & node_bit[n]:
69
+ covered |= allanc[n]
70
+ above[c] = sorted((n for n in nodes if a & ~covered & node_bit[n]), key=sort_key)
71
+ if above[c]:
72
+ own = int(git("rev-list", "--count", c, *["^" + p for p in above[c]]) or 1)
73
+ folded[c] = own - 1
74
+
75
+ def node_text(c):
76
+ parts = []
77
+ if c == head:
78
+ parts.append(paint(color.GREEN_BOLD, "HEAD -> " + branch if branch else "HEAD (detached)"))
79
+ parts += [paint(color.CYAN, n) for n in sorted(names.get(c, []))]
80
+ if not parts:
81
+ parts.append(paint(color.DIM, "merge-base"))
82
+ text = "%s %s" % (paint(color.DIM, c[:8]), ", ".join(parts))
83
+ return text + " " + head_note if c == head and head_note else text
84
+
85
+ def marker(c):
86
+ if c == head:
87
+ return paint(color.GREEN_BOLD, "@")
88
+ return paint(color.CYAN if c in names else color.DIM, "○") # ○
89
+
90
+ out = []
91
+ lanes = [] # lanes[i] = the node lane i is heading to (None = free)
92
+
93
+ def emit(cells, fill=(), width=0, text=""):
94
+ """One row. cells: lane -> glyph (default: a vertical line on an active lane);
95
+ fill: lanes joined to the next one by a horizontal line."""
96
+ n = max([i for i, c in enumerate(lanes) if c is not None] + [width - 1, *cells]) + 1
97
+ row = ""
98
+ for i in range(n):
99
+ glyph = cells.get(i) or ("│" if i < len(lanes) and lanes[i] else " ")
100
+ row += paint(color.DIM, glyph) if glyph in LINE_GLYPHS else glyph
101
+ row += paint(color.DIM, "─") if i in fill else " "
102
+ out.append(("%s %s" % (row, text)).rstrip())
103
+
104
+ def free_lane(after=-1):
105
+ for i in range(after + 1, len(lanes)):
106
+ if lanes[i] is None:
107
+ return i
108
+ lanes.append(None)
109
+ return len(lanes) - 1
110
+
111
+ def draw(c):
112
+ waiting = [i for i, n in enumerate(lanes) if n == c]
113
+ if not waiting:
114
+ k = free_lane()
115
+ lanes[k] = c
116
+ waiting = [k]
117
+ k, extra = waiting[0], waiting[1:]
118
+ if extra: # lanes of other children converge onto this node
119
+ cells = {k: "├", **{j: "┴" for j in extra}, extra[-1]: "╯"}
120
+ for i in range(k + 1, extra[-1]):
121
+ cells.setdefault(i, "┼" if lanes[i] else "─")
122
+ emit(cells, fill=range(k, extra[-1]))
123
+ for j in extra:
124
+ lanes[j] = None
125
+ emit({k: marker(c)}, width=k + 1, text=node_text(c))
126
+
127
+ ps = above[c]
128
+ lanes[k] = ps[0] if ps else None
129
+ for p in ps[1:]: # merge: a second parent gets its own lane (or joins the one already heading there)
130
+ joined = p in lanes
131
+ j = lanes.index(p) if joined else free_lane(k)
132
+ lanes[j] = p
133
+ lo, hi = min(k, j), max(k, j)
134
+ cells = {lo: "├", hi: "┤" if joined else "╮"}
135
+ for i in range(lo + 1, hi):
136
+ cells[i] = "┼" if lanes[i] else "─"
137
+ emit(cells, fill=range(lo, hi))
138
+ if folded.get(c):
139
+ emit({k: "~"}, text=paint(color.YELLOW, "(%d)" % folded[c]))
140
+
141
+ pending = {c: 0 for c in nodes}
142
+ for c in nodes:
143
+ for a in above[c]:
144
+ pending[a] += 1
145
+ ready = [c for c in nodes if pending[c] == 0]
146
+ while ready:
147
+ ready.sort(key=sort_key)
148
+ c = ready.pop(0)
149
+ draw(c)
150
+ for a in above[c]:
151
+ pending[a] -= 1
152
+ if pending[a] == 0:
153
+ ready.append(a)
154
+ out.append(paint(color.DIM, LEGEND))
155
+ return out
git_nearest/refs.py ADDED
@@ -0,0 +1,40 @@
1
+ """Rank remote refs by how close they are to HEAD."""
2
+ from collections import namedtuple
3
+
4
+ from .gitutil import git
5
+
6
+ REMOTES = "refs/remotes/"
7
+
8
+ Row = namedtuple("Row", "ahead behind ref") # ref is the short name, e.g. origin/main
9
+
10
+
11
+ def _counts_native():
12
+ """One git call for every ref; needs git >= 2.41 (%(ahead-behind:...))."""
13
+ out = git("for-each-ref", "--format=%(ahead-behind:HEAD)%09%(refname)", REMOTES, check=False)
14
+ if not out:
15
+ return None
16
+ return [line.split("\t") for line in out.splitlines()]
17
+
18
+
19
+ def _counts_fallback():
20
+ """One rev-list per ref, for older git."""
21
+ names = git("for-each-ref", "--format=%(refname)", REMOTES).splitlines()
22
+ pairs = []
23
+ for name in names:
24
+ if not name.endswith("/HEAD"):
25
+ pairs.append((git("rev-list", "--left-right", "--count", name + "...HEAD"), name))
26
+ return pairs
27
+
28
+
29
+ def rank(limit, native=True):
30
+ """Closest remote refs first: fewest commits HEAD is ahead by, then fewest it is behind by."""
31
+ pairs = (_counts_native() if native else None) or _counts_fallback()
32
+ rows = []
33
+ for counts, name in pairs:
34
+ if name.endswith("/HEAD"): # origin/HEAD is just a pointer
35
+ continue
36
+ # "<only in ref = behind> <only in HEAD = ahead>"
37
+ behind, ahead = counts.split()
38
+ rows.append(Row(int(ahead), int(behind), name[len(REMOTES):]))
39
+ rows.sort(key=lambda r: (r.ahead, r.behind, r.ref))
40
+ return rows[:limit]
git_nearest/status.py ADDED
@@ -0,0 +1,31 @@
1
+ """Working tree state of HEAD: is it clean, and if not, how dirty."""
2
+ from collections import namedtuple
3
+
4
+ from . import color
5
+ from .gitutil import git
6
+
7
+ Counts = namedtuple("Counts", "staged modified untracked conflicted")
8
+
9
+
10
+ def read():
11
+ counts = dict(staged=0, modified=0, untracked=0, conflicted=0)
12
+ for line in git("status", "--porcelain=v1", strip=False).splitlines():
13
+ x, y = line[0], line[1]
14
+ if x == "!":
15
+ continue
16
+ if x == "?":
17
+ counts["untracked"] += 1
18
+ elif "U" in (x, y) or (x, y) in (("A", "A"), ("D", "D")):
19
+ counts["conflicted"] += 1
20
+ else:
21
+ counts["staged"] += x != " "
22
+ counts["modified"] += y != " "
23
+ return Counts(**counts)
24
+
25
+
26
+ def describe(counts, paint):
27
+ """'[clean]' or e.g. '[2 staged, 3 modified, 1 untracked]'."""
28
+ parts = ["%d %s" % (n, name) for name, n in counts._asdict().items() if n]
29
+ if not parts:
30
+ return paint(color.GREEN, "[clean]")
31
+ return paint(color.RED if counts.conflicted else color.YELLOW, "[%s]" % ", ".join(parts))
git_nearest/table.py ADDED
@@ -0,0 +1,19 @@
1
+ """The default output: one line per ref with ahead/behind and the merge-base."""
2
+ from . import color
3
+ from .gitutil import git
4
+
5
+
6
+ def render(rows, paint, head_line=None):
7
+ ups = ["↑%d" % r.ahead for r in rows]
8
+ downs = ["↓%d" % r.behind for r in rows]
9
+ wa, wb, wn = max(map(len, ups)), max(map(len, downs)), max(len(r.ref) for r in rows)
10
+ lines = [head_line] if head_line else []
11
+ for r, up, down in zip(rows, ups, downs):
12
+ base = git("merge-base", "refs/remotes/" + r.ref, "HEAD", check=False)[:8]
13
+ lines.append("%s %s %s %s" % (
14
+ paint(color.GREEN, up.ljust(wa)),
15
+ paint(color.RED, down.ljust(wb)),
16
+ r.ref.ljust(wn),
17
+ paint(color.DIM, base),
18
+ ))
19
+ return lines
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.5
2
+ Name: git-nearest
3
+ Version: 0.1.0
4
+ Summary: Find the remote branches closest to your current branch, with ahead/behind counts and a compact jj-style graph.
5
+ Project-URL: Homepage, https://github.com/hwei/git-nearest
6
+ Project-URL: Issues, https://github.com/hwei/git-nearest/issues
7
+ Author-email: hwei <814065+hwei@users.noreply.github.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: branch,cli,git,merge-base,parent-branch
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Version Control :: Git
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+
19
+ # git-nearest
20
+
21
+ Which remote branch is my current branch closest to?
22
+
23
+ ```console
24
+ $ git nearest
25
+ @ HEAD -> fix-typo [1 staged, 1 untracked]
26
+ ↑2 ↓0 origin/main 574e9395
27
+ ↑8 ↓0 origin/feature/login 51eef8f5
28
+ ↑9 ↓1 origin/release/1.2 fc1afa3b
29
+ ```
30
+
31
+ The first line shows where HEAD is and the state of the working tree: `[clean]`, or counts such as
32
+ `[2 staged, 3 modified, 1 untracked]` (conflicted files turn it red). That is the thing to check
33
+ before moving the branch somewhere else.
34
+
35
+ `↑` is how many commits HEAD has that the remote branch doesn't, `↓` is how many the remote branch
36
+ has that HEAD doesn't, and the last column is the merge-base. The closest branch (fewest `↑`, then
37
+ fewest `↓`) comes first. Remote branches with the same tip all show up, so you can see ties
38
+ instead of having one picked for you.
39
+
40
+ ## Graph
41
+
42
+ `git nearest -g` draws how HEAD and those branches relate, in the style of
43
+ [`jj log`](https://docs.jj-vcs.dev/): newest on top, and runs of ordinary commits folded into a
44
+ `~ (n)` row on the vertical line.
45
+
46
+ ```console
47
+ $ git nearest -g
48
+ @ ae22273d HEAD -> fix-typo [1 staged, 1 untracked]
49
+ ~ (1)
50
+ ○ 574e9395 origin/main
51
+ ├─╮
52
+ ~ │ (3)
53
+ │ ○ 51eef8f5 origin/feature/login
54
+ │ ~ (2)
55
+ │ │ ○ 2cf4c0c2 origin/release/1.2
56
+ ├─┼─╯
57
+ ○ │ fc1afa3b merge-base
58
+ ~ │ (1)
59
+ ├─╯
60
+ ○ 5a42fea0 merge-base
61
+ ~ (n) = n commits folded between
62
+ ```
63
+
64
+ Only HEAD, the branch tips and the merge-bases between them are drawn. `(3)` means three commits sit
65
+ between the nodes above and below it.
66
+
67
+ Both examples are real output from a synthetic repository built by
68
+ [`scripts/demo.py`](https://github.com/hwei/git-nearest/blob/main/scripts/demo.py).
69
+
70
+ ## Install
71
+
72
+ Needs Python 3.8+ and git. No other dependencies.
73
+
74
+ ```console
75
+ uv tool install git-nearest # or: pipx install git-nearest
76
+ ```
77
+
78
+ The package installs an executable called `git-nearest`; git runs any `git-<name>` on your `PATH` as
79
+ `git <name>`.
80
+
81
+ ## Usage
82
+
83
+ ```
84
+ git nearest [N] [-g] [--no-status] [--color {auto,always,never}] [--no-color]
85
+ ```
86
+
87
+ | | |
88
+ |---|---|
89
+ | `N` | how many branches to show (default 5) |
90
+ | `-g`, `--graph` | draw the graph instead of the table |
91
+ | `--no-status` | skip the working tree check (it runs `git status`, which can be slow on huge repositories) |
92
+ | `--color` | `auto` (default) colors only on a terminal and respects `NO_COLOR` |
93
+
94
+ ## How it works
95
+
96
+ git has no notion of a branch's "parent", only of common ancestors, so this is a heuristic, not a
97
+ lookup. For every ref under `refs/remotes` it asks git for the ahead/behind counts against HEAD in a
98
+ single `for-each-ref --format='%(ahead-behind:HEAD)'` call (git 2.41+; older versions fall back to one
99
+ `rev-list` per ref, which is slower on repositories with many branches).
100
+
101
+ The graph walks the history above the common base once, marks which tips can reach each commit, and
102
+ keeps only the commits where those sets change. Those are the merge-bases.
103
+
104
+ ## Limitations
105
+
106
+ - "Nearest" is by commit count. If you branched from `main` last week and `release/1.0` happens to be
107
+ two commits behind `main`, `release/1.0` is nearer. Look at the graph when it matters.
108
+ - Branches that are not in `refs/remotes` are ignored. Run `git fetch` first.
109
+ - Folded counts on a node with two parents are the commits unique to that node, shown once.
110
+
111
+ ## Similar tools
112
+
113
+ - Scripts based on `git show-branch` or on grepping `git log --decorate` for `origin/` (the usual
114
+ Stack Overflow answers) look only at the first decorated ancestor, so they miss ties and give no
115
+ distance.
116
+ - [Git Town](https://www.git-town.com/) records a parent for each branch when it creates the branch.
117
+ That is exact but only works for branches it knows about; `git-nearest` infers from history and needs
118
+ no setup.
119
+
120
+ ## Development
121
+
122
+ ```console
123
+ uv run pytest
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,14 @@
1
+ git_nearest/__init__.py,sha256=T4bDFCMF22YiEOc93D53WE-XGn-VsNiY2FfTEh2iOC8,71
2
+ git_nearest/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
+ git_nearest/cli.py,sha256=aCOIFDjWFZPFsxdaJgxmIPgmXFqpjo_e7dcp97sVaqU,2566
4
+ git_nearest/color.py,sha256=2dFNZECboq2fU2hII0E2a48uGVmwL0pSFD_0bY7W5kU,359
5
+ git_nearest/gitutil.py,sha256=fHjnG0JdiRK0WcJkHZ0GJ7X3xAdbCn7wKWCjJ5cJ4_U,705
6
+ git_nearest/graph.py,sha256=XCHILZimX3vVcKoGuuXeEZPEwN-J3pcGqsOZ7_Vjf6A,6221
7
+ git_nearest/refs.py,sha256=FVTqehUsIbKXemodUBK4FaMZUfvWhbvRQoM98t2u6vk,1451
8
+ git_nearest/status.py,sha256=QrpLDED4TCeIXV-m75KVEB5fbpgVwGBVrVU1yZzdQSI,1074
9
+ git_nearest/table.py,sha256=VeVs-Hm52qUhXwHqDCIH5F2rKoGR09YLuqoOaVHLMkQ,749
10
+ git_nearest-0.1.0.dist-info/METADATA,sha256=RgahOC7xLdRFS4QmCE-3ATgWhUilotcn1j_KhitIBYA,4594
11
+ git_nearest-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
12
+ git_nearest-0.1.0.dist-info/entry_points.txt,sha256=CzRI0mP0b4MM41-_fu1Rww5bqupwt43qNUGAMsg4kpE,53
13
+ git_nearest-0.1.0.dist-info/licenses/LICENSE,sha256=KCGRMaq5ZibfutyPc2GJRMNoB4QoR4YiPrrgHFGPsE4,1061
14
+ git_nearest-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ git-nearest = git_nearest.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hwei
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.