Skip to content

Commit 8666a36

Browse files
Add git stash (#1228)
* Add placeholders * initial test setup * clean stash handler * Edit initial stash test parameters * Fixed payload parameters and add env * Added git stash failure test * Add dummy button to stash changes * Added placeholder stash section to GitPanel * Add git stash UI for each file * Added dummy button UI * Added git stash pop draft * Add refresh stash whenever git stash actions are callled * Add git stash drop/clear * Add git stash apply * Add stash signal * Add git stash message * Revert stashed files on stash * Fix revert files on stash * Fix stash message * Update stash signal * Re-render files on git stash apply * Fix stash pop * Style Git Stash Files * Style GitStash header label * Style stash heading buttons appear only on hover * Style stash entries * Add PR feedback * Refactor handler and git commands * Refactor based on PR comments * Finish handler tests * Remove async mock dependency * Update styles * Fix stash signal failing * Add PR feedback * Clean up comments * Fix prettier formatting * Add integration test * Add integration tests * Finish integration tests * Remove strict MacOS navigation * Use expect.soft except for last expect * Remove redundant aria label * Add timeout for stash text to disappear * Clean up code * [skip ci] Improve naming and doc * Better naming * Fix errors * Lint the code --------- Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>
1 parent f1e3fde commit 8666a36

File tree

16 files changed

+1893
-35
lines changed

16 files changed

+1893
-35
lines changed

jupyterlab_git/git.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,142 @@ def ensure_git_credential_cache_daemon(
17721772
elif self._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS.poll():
17731773
self.ensure_git_credential_cache_daemon(socket, debug, True, cwd, env)
17741774

1775+
async def stash(self, path: str, stashMsg: str = "") -> dict:
1776+
"""
1777+
Stash changes in a dirty working directory away
1778+
path: str Git path repository
1779+
stashMsg (optional): str
1780+
A message that describes the stash entry
1781+
"""
1782+
cmd = ["git", "stash"]
1783+
1784+
if len(stashMsg) > 0:
1785+
cmd.extend(["save", "-m", stashMsg])
1786+
1787+
env = os.environ.copy()
1788+
# if the git command is run in a non-interactive terminal, it will not prompt for user input
1789+
env["GIT_TERMINAL_PROMPT"] = "0"
1790+
1791+
code, output, error = await execute(cmd, cwd=path, env=env)
1792+
1793+
# code 0: no changes to stash
1794+
if code != 0:
1795+
return {"code": code, "command": " ".join(cmd), "message": error}
1796+
return {"code": code, "message": output, "command": " ".join(cmd)}
1797+
1798+
async def stash_list(self, path: str) -> dict:
1799+
"""
1800+
Execute git stash list command
1801+
"""
1802+
cmd = ["git", "stash", "list"]
1803+
1804+
env = os.environ.copy()
1805+
env["GIT_TERMINAL_PROMPT"] = "0"
1806+
1807+
code, output, error = await execute(cmd, cwd=path, env=env)
1808+
1809+
if code != 0:
1810+
return {"code": code, "command": " ".join(cmd), "message": error}
1811+
1812+
return {"code": code, "message": output, "command": " ".join(cmd)}
1813+
1814+
async def stash_show(self, path: str, index: int) -> dict:
1815+
"""
1816+
Execute git stash show command
1817+
"""
1818+
# stash_index = "stash@{" + str(index) + "}"
1819+
stash_index = f"stash@{{{index!s}}}"
1820+
1821+
cmd = ["git", "stash", "show", "-p", stash_index, "--name-only"]
1822+
1823+
env = os.environ.copy()
1824+
env["GIT_TERMINAL_PROMPT"] = "0"
1825+
1826+
code, output, error = await execute(cmd, cwd=path, env=env)
1827+
1828+
if code != 0:
1829+
return {"code": code, "command": " ".join(cmd), "message": error}
1830+
1831+
return {"code": code, "message": output, "command": " ".join(cmd)}
1832+
1833+
async def pop_stash(self, path: str, stash_index: Optional[int] = None) -> dict:
1834+
"""
1835+
Execute git stash pop for a certain index of the stash list. If no index is provided, it will
1836+
1837+
path: str
1838+
Git path repository
1839+
stash_index: number
1840+
Index of the stash list is first applied to the current branch, then removed from the stash.
1841+
If the index is not provided, the most recent stash (index=0) will be removed from the stash.
1842+
"""
1843+
cmd = ["git", "stash", "pop"]
1844+
1845+
if stash_index:
1846+
cmd.append(str(stash_index))
1847+
1848+
env = os.environ.copy()
1849+
env["GIT_TERMINAL_PROMPT"] = "0"
1850+
1851+
code, output, error = await execute(cmd, cwd=path, env=env)
1852+
1853+
if code != 0:
1854+
return {"code": code, "command": " ".join(cmd), "message": error}
1855+
1856+
return {"code": code, "message": output, "command": " ".join(cmd)}
1857+
1858+
async def drop_stash(self, path, stash_index: Optional[int] = None) -> dict:
1859+
"""
1860+
Execute git stash drop to delete a single stash entry.
1861+
If not stash_index is provided, delete the entire stash.
1862+
1863+
path: Git path repository
1864+
stash_index: number or None
1865+
Index of the stash list to remove from the stash.
1866+
If None, the entire stash is removed.
1867+
"""
1868+
cmd = ["git", "stash"]
1869+
if stash_index is None:
1870+
cmd.append("clear")
1871+
else:
1872+
cmd.extend(["drop", str(stash_index)])
1873+
1874+
env = os.environ.copy()
1875+
env["GIT_TERMINAL_PROMPT"] = "0"
1876+
1877+
code, output, error = await execute(cmd, cwd=path, env=env)
1878+
1879+
if code != 0:
1880+
return {"code": code, "command": " ".join(cmd), "message": error}
1881+
1882+
return {"code": code, "message": output, "command": " ".join(cmd)}
1883+
1884+
async def apply_stash(self, path: str, stash_index: Optional[int] = None) -> dict:
1885+
"""
1886+
Execute git stash apply to apply a single stash entry to the repository.
1887+
If not stash_index is provided, apply the latest stash.
1888+
1889+
path: str
1890+
Git path repository
1891+
stash_index: number
1892+
Index of the stash list is applied to the repository.
1893+
"""
1894+
# Clear
1895+
cmd = ["git", "stash", "apply"]
1896+
1897+
if stash_index is not None:
1898+
cmd.append("stash@{" + str(stash_index) + "}")
1899+
1900+
env = os.environ.copy()
1901+
env["GIT_TERMINAL_PROMPT"] = "0"
1902+
1903+
code, output, error = await execute(cmd, cwd=path, env=env)
1904+
1905+
# error:
1906+
if code != 0:
1907+
return {"code": code, "command": " ".join(cmd), "message": error}
1908+
1909+
return {"code": code, "message": output, "command": " ".join(cmd)}
1910+
17751911
@property
17761912
def excluded_paths(self) -> List[str]:
17771913
"""Wildcard-style path patterns that do not support git commands.

jupyterlab_git/handlers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,112 @@ async def post(self, path: str = ""):
892892
self.finish(json.dumps(result))
893893

894894

895+
class GitStashHandler(GitHandler):
896+
"""
897+
Handler for 'git stash'. Stores the changes in the current branch
898+
"""
899+
900+
@tornado.web.authenticated
901+
async def post(self, path: str = "", stashMsg: str = ""):
902+
"""
903+
POST request handler for 'git stash'
904+
"""
905+
local_path = self.url2localpath(path)
906+
data = self.get_json_body()
907+
908+
response = await self.git.stash(local_path, data.get("stashMsg", ""))
909+
if response["code"] == 0:
910+
self.set_status(201)
911+
else:
912+
self.set_status(500)
913+
914+
self.finish(json.dumps(response))
915+
916+
@tornado.web.authenticated
917+
async def delete(self, path: str = ""):
918+
"""
919+
DELETE request handler to clear a single stash or the entire stash list in a Git repository
920+
"""
921+
local_path = self.url2localpath(path)
922+
stash_index = self.get_query_argument("stash_index", None)
923+
924+
# Choose what to erase
925+
if (stash_index is None) and (stash_index != 0):
926+
response = await self.git.drop_stash(local_path)
927+
else:
928+
response = await self.git.drop_stash(local_path, stash_index)
929+
930+
if response["code"] == 0:
931+
self.set_status(204)
932+
else:
933+
self.set_status(500)
934+
self.finish()
935+
936+
@tornado.web.authenticated
937+
async def get(self, path: str = ""):
938+
"""
939+
GET request handler for 'git stash list'
940+
"""
941+
# pass the path to the git stash so it knows where to stash
942+
local_path = self.url2localpath(path)
943+
index = self.get_query_argument("index", None)
944+
if index is None:
945+
response = await self.git.stash_list(local_path)
946+
else:
947+
response = await self.git.stash_show(local_path, int(index))
948+
949+
if response["code"] == 0:
950+
self.set_status(200)
951+
else:
952+
self.set_status(500)
953+
self.finish(json.dumps(response))
954+
955+
956+
class GitStashPopHandler(GitHandler):
957+
"""
958+
Grab all the files affected by each git stash
959+
"""
960+
961+
@tornado.web.authenticated
962+
async def post(self, path: str = ""):
963+
"""
964+
POST request handler to pop the latest stash unless an index was provided
965+
"""
966+
local_path = self.url2localpath(path)
967+
data = self.get_json_body()
968+
969+
response = await self.git.pop_stash(local_path, data.get("index"))
970+
971+
if response["code"] == 0:
972+
self.set_status(204)
973+
self.finish()
974+
else:
975+
self.set_status(500)
976+
self.finish(json.dumps(response))
977+
978+
979+
class GitStashApplyHandler(GitHandler):
980+
"""
981+
Apply the latest stash to the repository.
982+
"""
983+
984+
@tornado.web.authenticated
985+
async def post(self, path: str = ""):
986+
"""
987+
POST request handler to apply the latest stash unless an index was provided
988+
"""
989+
local_path = self.url2localpath(path)
990+
data = self.get_json_body()
991+
response = await self.git.apply_stash(local_path, data.get("index"))
992+
993+
if response["code"] == 0:
994+
self.set_status(201)
995+
else:
996+
self.set_status(500)
997+
998+
self.finish(json.dumps(response))
999+
1000+
8951001
def setup_handlers(web_app):
8961002
"""
8971003
Setups all of the git command handlers.
@@ -931,6 +1037,9 @@ def setup_handlers(web_app):
9311037
("/tags", GitTagHandler),
9321038
("/tag_checkout", GitTagCheckoutHandler),
9331039
("/add", GitAddHandler),
1040+
("/stash", GitStashHandler),
1041+
("/stash_pop", GitStashPopHandler),
1042+
("/stash_apply", GitStashApplyHandler),
9341043
]
9351044

9361045
handlers = [

0 commit comments

Comments
 (0)