Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add git stash #1228

Merged
merged 48 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0c16190
Add placeholders
shawnesquivel Feb 28, 2023
48ea868
initial test setup
shawnesquivel Mar 2, 2023
8a9298b
clean stash handler
shawnesquivel Mar 2, 2023
41cbadd
Edit initial stash test parameters
shawnesquivel Mar 2, 2023
032c63b
Fixed payload parameters and add env
shawnesquivel Mar 3, 2023
2373b81
Added git stash failure test
shawnesquivel Mar 7, 2023
0fa84a6
Add dummy button to stash changes
shawnesquivel Mar 8, 2023
e76c34f
Added placeholder stash section to GitPanel
shawnesquivel Mar 12, 2023
45cf6a8
Add git stash UI for each file
shawnesquivel Mar 14, 2023
da2a351
Added dummy button UI
shawnesquivel Mar 16, 2023
3998d2f
Added git stash pop draft
shawnesquivel Mar 18, 2023
b685b80
Add refresh stash whenever git stash actions are callled
shawnesquivel Mar 18, 2023
7a7262d
Add git stash drop/clear
shawnesquivel Mar 19, 2023
3c71354
Add git stash apply
shawnesquivel Mar 20, 2023
6f32a0b
Add stash signal
shawnesquivel Mar 21, 2023
e6358f0
Add git stash message
shawnesquivel Mar 24, 2023
3075f81
Revert stashed files on stash
shawnesquivel Mar 29, 2023
9c34bf3
Fix revert files on stash
shawnesquivel Mar 29, 2023
d1d2a59
Fix stash message
shawnesquivel Mar 29, 2023
4e2971d
Update stash signal
shawnesquivel Apr 1, 2023
55ee778
Re-render files on git stash apply
shawnesquivel Apr 1, 2023
3abd0a0
Fix stash pop
shawnesquivel Apr 1, 2023
110c597
Style Git Stash Files
shawnesquivel Apr 2, 2023
64f76ef
Style GitStash header label
shawnesquivel Apr 3, 2023
356c5ea
Style stash heading buttons appear only on hover
shawnesquivel Apr 3, 2023
f24df91
Style stash entries
shawnesquivel Apr 3, 2023
c484c56
Add PR feedback
shawnesquivel Apr 5, 2023
a393410
Refactor handler and git commands
shawnesquivel Apr 6, 2023
0328fa9
Refactor based on PR comments
shawnesquivel Apr 7, 2023
61250ba
Finish handler tests
shawnesquivel Apr 7, 2023
164d2e8
Remove async mock dependency
shawnesquivel Apr 8, 2023
dc8dfa8
Update styles
shawnesquivel Apr 13, 2023
6a01985
Fix stash signal failing
shawnesquivel Apr 17, 2023
604c7fe
Add PR feedback
shawnesquivel Apr 17, 2023
96f0c84
Clean up comments
shawnesquivel Apr 17, 2023
2b27cd4
Fix prettier formatting
shawnesquivel Apr 17, 2023
8efdb19
Add integration test
shawnesquivel Apr 18, 2023
7e3f43d
Add integration tests
shawnesquivel Apr 18, 2023
08117f4
Finish integration tests
shawnesquivel Apr 20, 2023
857ba6d
Remove strict MacOS navigation
shawnesquivel Apr 20, 2023
7f3181b
Use expect.soft except for last expect
shawnesquivel Apr 20, 2023
025919b
Remove redundant aria label
shawnesquivel Apr 20, 2023
cf4aa6a
Add timeout for stash text to disappear
shawnesquivel Apr 20, 2023
59c36c2
Clean up code
shawnesquivel Apr 24, 2023
600614d
[skip ci] Improve naming and doc
fcollonval May 30, 2023
e277f78
Better naming
fcollonval May 30, 2023
7aa8641
Fix errors
fcollonval Jun 1, 2023
ad567a4
Lint the code
fcollonval Jun 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions jupyterlab_git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,146 @@ def ensure_git_credential_cache_daemon(
elif self._GIT_CREDENTIAL_CACHE_DAEMON_PROCESS.poll():
self.ensure_git_credential_cache_daemon(socket, debug, True, cwd, env)

async def stash(self, path: str, stashMsg: str = "") -> dict:
"""
Stash changes in a dirty working directory away
path: str Git path repository
stashMsg (optional): str
A message that describes the stash entry
"""
cmd = ["git", "stash"]

if len(stashMsg) > 0:
cmd.extend(["save", "-m", stashMsg])

env = os.environ.copy()
# if the git command is run in a non-interactive terminal, it will not prompt for user input
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

# code 0: no changes to stash
if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}
return {"code": code, "message": output, "command": " ".join(cmd)}

async def stash_list(self, path: str) -> dict:
"""
Execute git stash list command
"""
cmd = ["git", "stash", "list"]

env = os.environ.copy()
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}

return {"code": code, "message": output, "command": " ".join(cmd)}

async def stash_show(self, path: str, index: int) -> dict:
"""
Execute git stash show command
"""
# stash_index = "stash@{" + str(index) + "}"
stash_index = f"stash@{{{index!s}}}"

cmd = ["git", "stash", "show", "-p", stash_index, "--name-only"]

env = os.environ.copy()
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}

return {"code": code, "message": output, "command": " ".join(cmd)}

async def stash_pop(self, path: str, stash_index: Optional[int] = None) -> dict:
"""
Execute git stash pop for a certain index of the stash list. If no index is provided, it will

path: str
Git path repository
stash_index: number
Index of the stash list is first applied to the current branch, then removed from the stash.
If the index is not provided, the most recent stash (index=0) will be removed from the stash.

fcollonval marked this conversation as resolved.
Show resolved Hide resolved
"""
cmd = ["git", "stash", "pop"]

if stash_index:
cmd.append(str(stash_index))

env = os.environ.copy()
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

print(type(cmd[len(cmd) - 1]))
fcollonval marked this conversation as resolved.
Show resolved Hide resolved
if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}

return {"code": code, "message": output, "command": " ".join(cmd)}

async def stash_drop(self, path, stash_index: Optional[int] = None) -> dict:
"""
Execute git stash drop to delete a single stash entry.
If not stash_index is provided, delete the entire stash.

path: Git path repository
stash_index: number or None
Index of the stash list to remove from the stash.
If None, the entire stash is removed.
"""
cmd = ["git", "stash"]
print(stash_index, type(stash_index))
fcollonval marked this conversation as resolved.
Show resolved Hide resolved
if stash_index is None:
cmd.append("clear")
else:
cmd.extend(["drop", str(stash_index)])

env = os.environ.copy()
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}

return {"code": code, "message": output, "command": " ".join(cmd)}

async def stash_apply(self, path: str, stash_index: Optional[int] = None) -> dict:
"""
Execute git stash apply to apply a single stash entry to the repository.
If not stash_index is provided, apply the latest stash.

path: str
Git path repository
stash_index: number
Index of the stash list is applied to the repository.

fcollonval marked this conversation as resolved.
Show resolved Hide resolved
"""
# Clear
cmd = ["git", "stash", "apply"]

if stash_index is not None:
cmd.append("stash@{" + str(stash_index) + "}")

env = os.environ.copy()
env["GIT_TERMINAL_PROMPT"] = "0"

code, output, error = await execute(cmd, cwd=path, env=env)

# error:
if code != 0:
return {"code": code, "command": " ".join(cmd), "message": error}

return {"code": code, "message": output, "command": " ".join(cmd)}

shawnesquivel marked this conversation as resolved.
Show resolved Hide resolved
@property
def excluded_paths(self) -> List[str]:
"""Wildcard-style path patterns that do not support git commands.
Expand Down
110 changes: 110 additions & 0 deletions jupyterlab_git/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,113 @@ async def post(self, path: str = ""):
self.finish(json.dumps(result))


class GitStashHandler(GitHandler):
"""
Handler for 'git stash'. Stores the changes in the current branch
"""

@tornado.web.authenticated
async def post(self, path: str = "", stashMsg: str = ""):
"""
POST request handler for 'git stash'
"""
local_path = self.url2localpath(path)
data = self.get_json_body()

response = await self.git.stash(local_path, data.get("stashMsg", ""))
if response["code"] == 0:
self.set_status(201)
else:
self.set_status(500)

self.finish(json.dumps(response))

@tornado.web.authenticated
async def delete(self, path: str = ""):
"""
DELETE request handler to clear a single stash or the entire stash list in a Git repository
"""
local_path = self.url2localpath(path)
stash_index = self.get_query_argument("stash_index", None)

print(stash_index, type(stash_index))
shawnesquivel marked this conversation as resolved.
Show resolved Hide resolved
# Choose what to erase
if (stash_index is None) and (stash_index != 0):
response = await self.git.stash_drop(local_path)
else:
response = await self.git.stash_drop(local_path, stash_index)

if response["code"] == 0:
self.set_status(204)
else:
self.set_status(500)
self.finish()

@tornado.web.authenticated
async def get(self, path: str = ""):
"""
GET request handler for 'git stash list'
"""
# pass the path to the git stash so it knows where to stash
local_path = self.url2localpath(path)
index = self.get_query_argument("index", None)
if index is None:
response = await self.git.stash_list(local_path)
else:
response = await self.git.stash_show(local_path, int(index))

if response["code"] == 0:
self.set_status(200)
else:
self.set_status(500)
self.finish(json.dumps(response))


class GitStashPopHandler(GitHandler):
"""
Grab all the files affected by each git stash
"""

@tornado.web.authenticated
async def post(self, path: str = ""):
"""
POST request handler to pop the latest stash unless an index was provided
"""
local_path = self.url2localpath(path)
data = self.get_json_body()

response = await self.git.stash_pop(local_path, data.get("index"))

if response["code"] == 0:
self.set_status(204)
self.finish()
else:
self.set_status(500)
self.finish(json.dumps(response))


class GitStashApplyHandler(GitHandler):
"""
Apply the latest stash to the repository.
"""

@tornado.web.authenticated
async def post(self, path: str = ""):
"""
POST request handler to apply the latest stash unless an index was provided
"""
local_path = self.url2localpath(path)
data = self.get_json_body()
response = await self.git.stash_apply(local_path, data.get("index"))

if response["code"] == 0:
self.set_status(201)
else:
self.set_status(500)

self.finish(json.dumps(response))


shawnesquivel marked this conversation as resolved.
Show resolved Hide resolved
def setup_handlers(web_app):
"""
Setups all of the git command handlers.
Expand Down Expand Up @@ -931,6 +1038,9 @@ def setup_handlers(web_app):
("/tags", GitTagHandler),
("/tag_checkout", GitTagCheckoutHandler),
("/add", GitAddHandler),
("/stash", GitStashHandler),
("/stash_pop", GitStashPopHandler),
("/stash_apply", GitStashApplyHandler),
]

handlers = [
Expand Down
Loading