1 {-# LANGUAGE FlexibleContexts #-}
2 {-# LANGUAGE OverloadedLists #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE PartialTypeSignatures #-}
5 {-# LANGUAGE PackageImports #-}
6 {-# LANGUAGE LambdaCase #-}
7 {-# LANGUAGE MultiWayIf #-}
8 {-# LANGUAGE PatternSynonyms #-}
9 {-# LANGUAGE ScopedTypeVariables #-}
10 {-# LANGUAGE TupleSections #-}
11 {-# LANGUAGE TypeFamilies #-}
12 {-# LANGUAGE ViewPatterns #-}
13 {-# OPTIONS_GHC -Wall #-}
14 {-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
15 {-# OPTIONS_GHC -fno-warn-missing-signatures #-}
16 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
17 {-# OPTIONS_GHC -fno-warn-unused-imports #-}
18 {-# OPTIONS_GHC -fno-warn-unused-matches #-}
19 {-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
22 import Prelude
hiding (putStrLn,putStr,writeFile,readFile)
24 import Data
.ByteString
(ByteString
,uncons
)
25 import Data
.ByteString
.Char8
(putStrLn,putStr,pack
,hPutStrLn)
26 import Data
.List
(foldl', isPrefixOf)
27 import Data
.Maybe(fromMaybe,isJust,isNothing)
28 import Data
.Monoid
((<>))
29 import Data
.Typeable
(typeOf
)
30 import Control
.Monad
(foldM,forM_
,when)
31 import Control
.Monad
.Catch
(displayException
,finally
,catch,catchJust
,catches
,SomeException
,throwM
)
32 import Control
.Monad
.Catch
(MonadMask
,MonadThrow
,SomeException
(SomeException
),Exception
,Handler
(Handler
))
33 import Control
.Monad
.Fix
(fix
)
34 import Control
.Monad
.IO.Class
(liftIO
,MonadIO
)
35 import Control
.Monad
.Reader
(MonadReader
,ask
)
36 import Control
.Monad
.RWS
(execRWST
, RWST
, runRWST
)
37 import Control
.Monad
.State
(put
,get
,modify
',MonadState
)
38 import Control
.Monad
.Trans
(lift
)
39 import Control
.Monad
.Trans
.Reader
(ReaderT
(runReaderT
))
40 import Control
.Monad
.Trans
.State
(evalStateT
,execStateT
)
41 import Control
.Monad
.Trans
.Cont
(ContT
(ContT
),evalContT
)
42 import Control
.Monad
.Trans
.Writer
(execWriterT
)
43 import Control
.Monad
.Writer
(tell
)
44 import System
.Exit
(ExitCode(ExitSuccess
,ExitFailure
),exitWith)
45 import System
.IO(hClose,IOMode(WriteMode
,AppendMode
),hSetBinaryMode
)
47 import qualified Data
.ByteString
as ByteString
48 import qualified Data
.ByteString
.Char8
as BC
49 import qualified Data
.Map
.Strict
as Map
50 import qualified Data
.Set
as Set
51 import qualified GHC
.IO.Exception
as GIE
52 import qualified Prelude
as Prelude
53 import qualified System
.IO as SI
55 import Rehi
.Git
.Types
(Hash
(Hash
), hashString
)
56 import Rehi
.Utils
(equalWith
, index_only
, readPopen
, mapFileLinesM
, modifySnd
,
57 trim
, writeFile, appendToFile
, whenM
, unlessM
, ifM
, popen_lines
,
58 tryWithRethrowComandFailure
,onCommandFailure
)
59 import Rehi
.Utils
.ArgList
(ArgList
(ArgList
))
60 import Rehi
.Utils
.IO(withBinaryFile
,readBinaryFile
,openBinaryFile
,openBinaryTempFile
,callCommand
,
61 createDirectory,copyFile
,
62 removeDirectoryRecursive
,removeFile,doesFileExist,doesDirectoryExist, getArgs,
63 lookupEnv
, system, initProgram
)
64 import Rehi
.Utils
.Regex
(regex_match
, regex_match_with_newlines
, regex_match_all
, regex_split
)
66 import qualified Rehi
.Git
.Commands
as Cmd
69 main
= handleErrors
(SI
.hPutStrLn SI
.stderr) (hPutStrLn SI
.stderr) (exitWith . ExitFailure
) $ do
72 flip runReaderT env
$ do
73 args
<- liftIO
getArgs
74 let parsed
= parse_cli args
78 (todo
, current
, commits
, target_ref
, marks
) <- restore_rebase
81 run_continue c commits
82 liftIO
(removeFile (envGitDir env `mappend`
"/rehi/current"))
84 lift
$ run_rebase
(envGitDir env
) todo commits target_ref marks Sync
86 (todo
, current
, commits
, target_ref
, marks
) <- restore_rebase
89 liftIO
$ Cmd
.reset
$ "HEAD"
90 liftIO
(removeFile (envGitDir env `mappend`
"/rehi/current"))
91 lift
$ run_rebase
(envGitDir env
) todo commits target_ref marks Sync
93 let currentPath
= envGitDir env `mappend`
"/rehi/current"
94 liftIO
(doesFileExist currentPath
) `unlessM` throwM
(ExpectedFailure
["No rehi in progress"])
95 content
<- liftIO
$ readBinaryFile currentPath
96 liftIO
$ putStr ("Current: " <> content
<> (if ByteString
.null content || BC
.last content
/= '\n' then "\n" else ""))
97 Run dest source_from_arg through source_to_arg target_arg interactive
-> do
99 initial_branch
<- git_get_checkedout_branch
101 target_ref
= fromMaybe initial_branch target_arg
102 source_to
= fromMaybe target_ref source_to_arg
103 source_from
<- case source_from_arg
of
105 Nothing | Just _
<- regex_match
".*~1$" dest
-> pure dest
106 Nothing
-> git_merge_base source_to dest
108 through
' = case regex_match
"^(.*)~1$" source_from
of
109 Just
(_
: m
: _
) -> m
: through
111 main_run dest source_from through
' source_to target_ref initial_branch interactive
118 | Run
{ runDest
:: ByteString
119 , runFrom
:: (Maybe ByteString
)
120 , runThroughs
:: [ByteString
]
121 , runTo
:: (Maybe ByteString
)
122 , runTarget
:: (Maybe ByteString
)
123 , runInteractive
:: Bool }
126 data Head
= Sync | Known Hash
deriving Show
128 data Commits
= Commits
{
129 commitsRefs
:: Map
.Map ByteString Hash
130 , commitsByHash
:: Map
.Map Hash Entry
134 entryAHash
:: ByteString
136 , entrySubject
:: ByteString
137 , entryParents
:: [Hash
]
139 , entryBody
:: ByteString
148 | Merge
{ mergeRef
:: Maybe ByteString
, mergeParents
:: [ByteString
], mergeOurs
:: Bool, mergeNoff
:: Bool }
151 | UserComment ByteString
152 | TailPickWithComment ByteString ByteString
155 data Env a
= Env
{ envGitDir
:: ByteString
, envRest
:: a
}
160 , tsMarks
:: Map
.Map ByteString Hash
164 type TE
= Env Commits
168 teRefs
= commitsRefs
. envRest
170 teByHash
= commitsByHash
. envRest
172 pattern TE refs byHash
<- Env
{ envRest
= (Commits refs byHash
) }
174 data StepResult
= StepPause | StepNext
176 newtype EditError
= EditError ByteString
deriving Show
178 instance Exception EditError
180 newtype ExpectedFailure
= ExpectedFailure
[ByteString
] deriving Show
181 instance Exception ExpectedFailure
183 pattern CommandFailed location
<- GIE
.IOError { GIE
.ioe_type
= GIE
.OtherError
,
184 GIE
.ioe_location
= location
}
186 parse_cli
= parse_loop
False
188 parse_loop _
("-i" : argv
') = parse_loop
True argv
'
189 parse_loop _
("--interactive" : argv
') = parse_loop
True argv
'
190 parse_loop _ argv
@("--abort" : _
: _
) = error ("Extra argument:" ++ show argv
)
191 parse_loop _
["--abort"] = Abort
192 parse_loop _ argv
@("--continue" : _
: _
) = error ("Extra argument:" ++ show argv
)
193 parse_loop _
["--continue"] = Continue
194 parse_loop _ argv
@("--skip" : _
: _
) = error ("Extra argument:" ++ show argv
)
195 parse_loop _
["--skip"] = Skip
196 parse_loop _ argv
@("--current" : _
: _
) = error ("Extra argument:" ++ show argv
)
197 parse_loop _
["--current"] = Current
198 parse_loop interactive
[dest
] = Run dest Nothing
[] Nothing Nothing interactive
199 parse_loop interactive
(arg0
: arg1
: arg2mb
) |
length arg2mb
== 1 ||
length arg2mb
== 0 && isJust (regex_match
"\\.\\." arg1
) =
201 re_ref0
= "(?:[^\\.]|(?<!\\.)\\.)*"
202 re_ref1
= "(?:[^\\.]|(?<!\\.)\\.)+"
203 re_sep
= "(?<!\\.)\\.\\."
204 (source_from
, through
, source_to
) = case regex_match
(mconcat
["^(", re_ref0
, ")", re_sep
, "((?:", re_ref1
, re_sep
, ")*)(", re_ref0
, ")$"]) arg1
of
205 Just
[all, m1
, m2
, m3
] -> (m1
, regex_match_all m2
(mconcat
["(", re_ref1
, ")", re_sep
]), m3
)
206 _
-> error ("Invalid source spec:" ++ show arg1
)
207 arg2
= case arg2mb
of
210 maybeFromString
"" = Nothing
211 maybeFromString s
= Just s
212 in Run arg0
(maybeFromString source_from
) through
(maybeFromString source_to
) arg2 interactive
213 parse_loop interactive
[arg0
, arg1
] = Run arg0 Nothing
[] Nothing
(Just arg1
) interactive
214 parse_loop _ argv
= error ("Invalid arguments: " ++ show argv
)
216 main_run
:: ByteString
-> ByteString
-> [ByteString
] -> ByteString
-> ByteString
-> ByteString
-> Bool -> ReaderT
(Env
()) IO ()
217 main_run dest source_from through source_to target_ref initial_branch interactive
= do
218 (todo
, commits
, dest_hash
) <-
220 (init_rebase dest source_from through source_to target_ref initial_branch
)
221 (\(e
:: ExpectedFailure
) -> do
224 (todo
, commits
) <- if interactive
226 let todo
' = add_info_to_todo todo commits
227 edit_todo todo
' commits
>>= \case
228 Just todo
-> pure
(todo
, commits
)
231 throwM
(ExpectedFailure
["Aborted"]))
232 else pure
(todo
, commits
)
233 if any (\case { UserComment _
-> False ; _
-> True }) todo
236 liftIO
$ save_todo todo
(gitDir
<> "/rehi/todo.backup") commits
237 liftIO
$ Cmd
.checkout_detached
$ hashString dest_hash
238 lift
$ run_rebase gitDir todo commits target_ref Map
.empty (Known dest_hash
))
240 liftIO
(putStrLn "Nothing to do")
245 target_ref
<- liftIO
(readBinaryFile
(gitDir
<> "/rehi/target_ref"))
246 (commits
, marks
) <- git_load_commits
247 todo
<- read_todo
(gitDir
<> "/rehi/todo") commits
248 current
<- ifM
(liftIO
(doesFileExist (gitDir
<> "/rehi/current")))
250 [step
] <- read_todo
(gitDir
<> "/rehi/current") commits
253 pure
(todo
, current
, commits
, target_ref
, marks
)
255 init_rebase
:: _
-> _
-> _
-> _
-> _
-> _
-> ReaderT
(Env a
) IO ([_
], _
, _
)
256 init_rebase dest source_from through source_to target_ref initial_branch
= do
257 (dest_hash
: source_from_hash
: source_to_hash
: through_hashes
) <-
258 liftIO
$ Cmd
.git_resolve_hashes
(dest
: source_from
: source_to
: through
)
259 init_save target_ref initial_branch
260 commits
<- git_fetch_cli_commits source_from source_to
261 let unknown_parents
= find_unknown_parents commits
262 commits
<- git_fetch_commit_list commits unknown_parents
263 case build_rebase_sequence commits source_from_hash source_to_hash through_hashes
of
264 Right todo
-> pure
(todo
, commits
, dest_hash
)
265 Left msg
-> throwM
$ ExpectedFailure
[msg
]
267 find_unknown_parents commits
=
268 Set
.toList
$ Set
.fromList
[ p | c
<- Map
.elems (commitsByHash commits
),
270 not (Map
.member p
(commitsByHash commits
)) ]
284 comments_from_string
:: ByteString
-> Int -> [Step
]
285 comments_from_string content indent
=
286 map (\l
-> UserComment
(mconcat
(replicate indent
" ") <> l
))
287 (regex_split content
"\\r\\n|\\r|\\n")
289 add_info_to_todo old_todo commits
= old_todo
++ comments_from_string help
0 ++ [UserComment
"", UserComment
" Commits"] ++ comments
291 comments
= concatMap (\case
292 Pick ah
-> from_hash ah
293 Fixup ah
-> from_hash ah
294 Edit ah
-> from_hash ah
295 Merge
(Just ah
) _ _ _
-> from_hash ah
297 from_hash ah
= fromMaybe [] (do
298 h
<- Map
.lookup ah
(commitsRefs commits
)
299 e
<- Map
.lookup h
(commitsByHash commits
)
300 pure
([UserComment
("----- " <> ah
<> " -----")] ++ comments_from_string
(entryBody e
) 0))
302 edit_todo old_todo commits
= do
304 (todoPath
, todoHandle
) <- liftIO
(openBinaryTempFile
(gitDir
<> "/rehi") "todo.XXXXXXXX")
305 liftIO
(hClose todoHandle
)
306 liftIO
$ save_todo old_todo todoPath commits
308 -- use git to launch editor to avoid dealing with msys paths in Windows
309 liftIO
$ Cmd
.edit_config_file todoPath
310 todo_rc
<- read_todo todoPath commits
314 verify_marks todo
= do
315 _
<- foldM (\marks
-> \case
316 Mark m | Set
.member m marks
-> throwM
(EditError
("Duplicated mark: " <> m
))
317 Mark m
-> pure
$ Set
.insert m marks
318 Pick ref
-> check marks ref
319 Fixup ref
-> check marks ref
320 Edit ref
-> check marks ref
321 Reset ref
-> check marks ref
322 Merge _ refs _ _
-> mapM_ (check marks
) refs
>> pure marks
323 UserComment _
-> pure marks
324 TailPickWithComment _ _
-> pure marks
325 Comment _
-> pure marks
326 Exec _
-> pure marks
) Set
.empty todo
329 check marks
(uncons
-> Just
((== (ByteString
.head "@")) -> True, mark
)) |
not (Set
.member mark marks
) = throwM
(EditError
("Unknown mark:" <> mark
))
330 check marks _
= pure marks
332 run_continue
:: (MonadReader
(Env a
) m
, MonadIO m
, MonadThrow m
) => Step
-> t
-> m
()
333 run_continue current commits
= do
335 tryWithRethrowComandFailure
336 ["callProcess: ", "readCreateProcess: "]
337 (ExpectedFailure
["Continue failed - unresolved problems"])
340 Pick ah
-> git_no_uncommitted_changes `unlessM` liftIO
(Cmd
.commit
$ Just ah
)
341 Merge ahM _ _ _
-> git_no_uncommitted_changes `unlessM` liftIO
(Cmd
.commit ahM
)
342 Edit _
-> git_no_uncommitted_changes `unlessM` throwM
(ExpectedFailure
["No unstaged changes should be after 'edit'"])
343 Fixup _
-> git_no_uncommitted_changes `unlessM` liftIO Cmd
.commit_amend
344 Exec cmd
-> throwM
$ ExpectedFailure
["Cannot continue '" <> cmd
<> "'", "resolve it manually, then skip or abort"]
345 Comment c
-> comment c
346 _
-> fail ("run_continue: Unexpected " ++ show current
)
348 data FinalizeMode
= CleanupData | KeepData
350 run_rebase gitDir todo commits target_ref marks curHead
=
352 (runReaderT doJob
(Env gitDir commits
))
360 liftIO
$ Cmd
.checkout_here target_ref
364 (catch :: _
-> (SomeException
-> _
) -> _
)
367 liftIO
$ Prelude
.putStrLn ("Fatal error: " <> show e
)
368 liftIO
$ putStrLn "Not possible to continue"
369 liftIO
$ removeFile (gitDir
<> "/rehi/todo"))
370 mainLoop
= fix
(\rec todo
-> do
372 (current
: todo
) -> do
373 let hasIo
= case current
of
374 UserComment _
-> False
375 TailPickWithComment _ _
-> False
378 commits
<- envRest
<$> ask
379 liftIO
$ save_todo todo
(gitDir
<> "/rehi/todo") commits
380 liftIO
$ save_todo
[current
] (gitDir
<> "/rehi/current") commits
)
381 run_step current
>>= \case
382 StepPause
-> pure KeepData
384 when hasIo
$ liftIO
(removeFile (gitDir
<> "/rehi/current"))
386 [] -> pure CleanupData
) todo
390 initial_branch
<- liftIO
$ readBinaryFile
(gitDir
<> "/rehi/initial_branch")
391 liftIO
$ Cmd
.reset initial_branch
392 liftIO
$ Cmd
.checkout_force initial_branch
400 run_step rebase_step
= do
404 pick
=<< resolve_ahash ah
406 commits
<- envRest
<$> ask
407 liftIO
$ putStrLn ("Apply: " <> commits_get_subject commits ah
)
408 pick
=<< resolve_ahash ah
410 liftIO
$ Prelude
.putStrLn "Amend the commit and run \"git rehi --continue\""
411 returnC
$ pure StepPause
413 commits
<- envRest
<$> ask
414 liftIO
$ putStrLn ("Fixup: " <> commits_get_subject commits ah
)
416 (liftIO
. Cmd
.fixup
) =<< resolve_ahash ah
418 hash_or_ref
<- resolve_ahash ah
419 fmap (Map
.member
(Hash hash_or_ref
) . teByHash
) ask
>>= \case
420 True -> modify
' (\ts
-> ts
{ tsHead
= Known
$ Hash hash_or_ref
})
422 liftIO
$ Cmd
.reset hash_or_ref
423 modify
' (\ts
-> ts
{tsHead
= Sync
})
427 $ tryWithRethrowComandFailure
430 [ "Command " <> cmd
<> " failed."
431 , "Resolve and run `git rehi --skip` or `git rehi --abort`"])
433 Comment new_comment
-> do
434 liftIO
$ putStrLn "Updating comment"
437 Mark mrk
-> add_mark mrk
438 Merge commentFrom parents ours noff
-> merge commentFrom parents ours noff
439 UserComment _
-> pure
()
443 hashNow
<- fmap tsHead get
>>= \case
446 [hashNow
] <- liftIO
$ Cmd
.git_resolve_hashes
["HEAD"]
448 modify
' $ \ts
-> ts
{ tsMarks
= Map
.insert mrk hashNow
(tsMarks ts
) }
450 liftIO
$ appendToFile
(gitDir
<> "/rehi/marks") (mrk
<> " " <> hashString hashNow
<> "\n")
452 merge commit_refMb merge_parents_refs ours noff
= do
453 fmap ((,commit_refMb
) . tsHead
) get
>>= \case
454 (Known cachedHash
, Just commit_ref
) -> do
455 (Commits refs byHash
) <- envRest
<$> ask
457 _ | Just step_hash
<- Map
.lookup commit_ref refs
458 , Just step_data
<- Map
.lookup step_hash byHash
459 -> fix
(\rec actuals expects
->
460 case (actuals
, expects
) of
461 ("HEAD" : at
, eh
: et
) -> if eh
== cachedHash
then rec at et
else merge_new_
462 (ah
: at
, eh
: et
) -> do
463 ahHash
<- resolve_ahash ah
464 if ByteString
.isPrefixOf ahHash
(hashString eh
) then rec at et
else merge_new_
466 liftIO
$ putStrLn ("Fast-forwarding unchanged merge: " <> commit_ref
<> " " <> entrySubject step_data
)
467 modify
' (\s
-> s
{tsHead
= Known step_hash
})
469 merge_parents_refs
(entryParents step_data
)
470 |
otherwise -> merge_new_
473 merge_new_
= merge_new commit_refMb merge_parents_refs ours noff
475 merge_new
:: (MonadIO m
, MonadState TS m
, MonadReader TE m
) => Maybe ByteString
-> [ByteString
] -> Bool -> Bool -> m
()
476 merge_new commit_refMb parents_refs ours noff
= do
477 [oldHead
] <- fmap tsHead get
>>= \case
478 Known hash
-> pure
[hash
]
479 Sync
-> liftIO
$ Cmd
.git_resolve_hashes
["HEAD"]
481 liftIO
$ putStrLn "Merging"
482 parents
<- mapM resolve_ahash parents_refs
483 let head_pos
= index_only
"HEAD" parents_refs
484 parents
<- if head_pos
/= 0
487 (pFirst
: pInit
, _
: pTail
) = splitAt head_pos parents
488 liftIO
$ Cmd
.reset pFirst
489 pure
(pInit
++ [hashString oldHead
] ++ pTail
)
490 else pure
(tail parents
)
491 liftIO
$ tryWithRethrowComandFailure
493 (ExpectedFailure
["Merge failed. Resolve and --continue or --skip, or --abort"])
494 (Cmd
.merge
(isNothing commit_refMb
) ours noff parents
)
496 Just commit
-> liftIO
$
497 tryWithRethrowComandFailure
500 ["Merge commit failed, fastforward?"
501 , "You would probably want to do now one of the following:"
502 , " * force non-fastforward merge"
503 , " * merge some other parent instead of the one from history"
504 , " * skip the step at all (is it a non-interactive rebase?)"
505 , "Then invoke git rehi --skip"])
506 (Cmd
.commit_refMsgOnly commit
)
509 sync_head
:: (MonadState TS m
, MonadIO m
) => m
()
511 fmap tsHead get
>>= \case
513 liftIO
$ Cmd
.reset
$ hashString hash
514 modify
' (\t -> t
{tsHead
= Sync
})
522 | Just pickData
<- Map
.lookup (Hash hash
) (teByHash env
)
523 , [pickParent
] <- (entryParents pickData
)
524 , pickParent
== currentHash
526 liftIO
$ putStrLn ("Fast-forwarding unchanged commit: " <> entryAHash pickData
<> " " <> entrySubject pickData
)
527 modify
' (\s
-> s
{ tsHead
= Known
(Hash hash
)})
535 execWriterT
(liftIO
(popen_lines
"git" "status --porcelain -uno" '\n') >>= mapM_ (\case
536 (regex_match
"^[DAU][DAU] (.*)$" -> Just
[_
, f
]) -> tell
[f
]
538 throwM
$ ExpectedFailure
([ "Conflicting files:" ] ++
539 (map (" " <>) conflicting_files
) ++
542 <> "` failed. Resolve and --continue or --skip, or --abort" ]))
543 (Cmd
.cherrypick hash
)
545 comment new_comment
= do
547 liftIO
$ writeFile (gitDir
<> "/rehi/commit_msg") new_comment
548 liftIO
$ Cmd
.commit_amend_msgFile
(gitDir
<> "/rehi/commit_msg")
550 build_rebase_sequence
:: Commits
-> Hash
-> Hash
-> [Hash
] -> Either ByteString
[Step
]
551 build_rebase_sequence commits source_from_hash source_to_hash through_hashes
=
552 case find_sequence
(commitsByHash commits
) source_from_hash source_to_hash through_hashes
of
557 (\(marks
, mark_num
, prev_hash
) step_hash
->
558 let (marks
', mark_num
') =
560 (\v@(marks
, mark_num
) parent
->
561 case Map
.lookup parent marks
of
563 (Map
.insert parent
(Just
("tmp_" <> pack
(show mark_num
))) marks
567 (filter (/= prev_hash
) $ entryParents
(commitsByHash commits Map
.! step_hash
))
568 in (marks
', mark_num
', step_hash
))
569 (Map
.fromList
$ zip ([source_from_hash
] ++ sequence) (repeat Nothing
)
573 from_mark
= maybe [] ((:[]) . Mark
) (marks Map
.! source_from_hash
)
574 steps
= concat $ zipWith makeStep
sequence (source_from_hash
: sequence)
575 makeStep this prev
= reset
++ step
++ maybe [] ((:[]) . Mark
) (marks Map
.! this
)
577 thisE
= commitsByHash commits Map
.! this
579 if prev `
elem` entryParents thisE
581 else case filter (`Map
.member` marks
) (entryParents thisE
) of
582 (h
: _
) | Just m
<- marks Map
.! h
-> (h
, [Reset
("@" <> m
)])
583 | Nothing
<- marks Map
.! h
-> error ("Unresolved mark for " <> show h
)
584 [] -> error ("No known parents for found step " <> show this
)
585 step
= case entryParents thisE
of
586 [p
] -> [Pick
$ entryAHash thisE
]
587 ps
-> make_merge_steps thisE real_prev commits marks
588 in Right
(from_mark
++ steps
)
591 make_merge_steps thisE real_prev commits marks
= singleHead `
seq`
[Merge
(Just ahash
) parents ours
False]
593 parents
= map mkParent
(entryParents thisE
)
594 mkParent p | p
== real_prev
= "HEAD"
595 | Just
(Just m
) <- Map
.lookup p marks
= "@" <> m
596 | Just Nothing
<- Map
.lookup p marks
= error ("Unresolved mark for " <> show p
)
597 | Just e
<- Map
.lookup p
(commitsByHash commits
) = entryAHash e
598 |
True = error ("Unknown parent: " <> show p
)
599 singleHead
= index_only
"HEAD" parents
:: Integer
600 ahash
= entryAHash thisE
601 ours
= entryTree thisE
== entryTree
(commitsByHash commits Map
.! head (entryParents thisE
) )
603 git_fetch_cli_commits from to
= do
604 git_fetch_commits
("log -z --ancestry-path --pretty=format:%H:%h:%T:%P:%B" <> [from
<> ".." <> to
])
605 (Commits Map
.empty Map
.empty)
607 git_fetch_commits
:: (MonadIO m
, MonadMask m
, MonadReader
(Env a
) m
) => ArgList
-> Commits
-> m Commits
608 git_fetch_commits args commits
= do
610 h
<- liftIO
$ openBinaryFile
(gitDir
<> "/rehi/commits") (AppendMode
)
611 liftIO
$ hSetBinaryMode h
True
615 ((liftIO
$ popen_lines
"git" args
'\0') >>= mapM (\case
618 git_parse_commit_line line
619 liftIO
$ BC
.hPut h line
))
623 git_load_commits
= do
625 commits
<- execStateT
(mapFileLinesM git_parse_commit_line
(gitDir
<> "/rehi/commits") '\0') commitsEmpty
626 let marksFile
= gitDir
<> "/rehi/marks"
627 marks
<- execStateT
(liftIO
(doesFileExist marksFile
) `whenM` mapFileLinesM addMark marksFile
'\n') Map
.empty
628 pure
(commits
, marks
)
630 addMark
(regex_match
"^([0-9a-zA-Z_\\/]+) ([0-9a-fA-F]+)$" -> Just
[_
, mName
, mValue
])
631 = modify
' (Map
.insert mName
(Hash mValue
))
632 addMark line
= fail ("Ivalid mark line: " <> show line
)
634 git_parse_commit_line line
= do
635 case regex_match_with_newlines
"^([0-9a-f]+):([0-9a-f]+):([0-9a-f]+):([0-9a-f ]*):(.*)$" line
of
636 Just
[_
, Hash
-> hash
, ahash
, Hash
-> tree
, map Hash
. BC
.split ' ' -> parents
, body
] -> do
638 mapM_ verify_hash parents
640 (subject
: _
) = BC
.split '\n' body
641 obj
= Entry ahash hash subject parents tree body
642 modify
' (\c
-> c
{ commitsByHash
= Map
.insertWith
(const id) hash obj
(commitsByHash c
)
643 , commitsRefs
= Map
.insertWith
(\hNew hOld
-> if hNew
== hOld
then hOld
else error ("Duplicated ref with different hash: " <> show ahash
<> "=>" <> show hOld
<> ", " <> show hNew
))
647 _
-> fail ("Could not parse line: " <> show line
)
649 git_merge_base b1 b2
= do
650 [base
] <- liftIO
$ popen_lines
"git" ("merge-base -a" <> [b1
, b2
]) '\n'
653 verify_hash
:: Monad m
=> Hash
-> m
()
654 verify_hash
(Hash
(regex_match
"^[0-9a-f]{40}$" -> Just _
)) = pure
()
655 verify_hash
(Hash h
) = fail ("Invalid hash: " <> show h
)
657 init_save target_ref initial_branch
= do
659 liftIO
(doesFileExist (gitDir
<> "/rehi")) `whenM`
fail "already in progress"
660 liftIO
$ createDirectory (gitDir
<> "/rehi")
661 liftIO
$ writeFile (gitDir
<> "/rehi/target_ref") target_ref
662 liftIO
$ writeFile (gitDir
<> "/rehi/initial_branch") initial_branch
664 cleanup_save
:: (MonadReader
(Env a
) m
, MonadIO m
) => m
()
667 liftIO
(doesDirectoryExist (gitDir
<> "/rehi")) `whenM`
(do
668 let newBackup
= gitDir
<> "/rehi/todo.backup"
669 liftIO
(doesFileExist newBackup
) `whenM`
670 liftIO
(copyFile newBackup
(gitDir
<> "/rehi_todo.backup"))
671 liftIO
$ removeDirectoryRecursive
(gitDir
<> "/rehi"))
673 commits_get_subject
(Commits refs byHash
) ah
= do
675 (\h
-> maybe "???" entrySubject
$ Map
.lookup h byHash
)
678 save_todo todo path commits
= do
680 (reverse -> tail, reverse -> main
) = span
(\case { UserComment _
-> True; TailPickWithComment _ _
-> True; _
-> False }) $ reverse todo
681 withBinaryFile path WriteMode
$ \out
-> do
682 forM_ main
$ hPutStrLn out
. \case
683 Pick ah
-> "pick " <> ah
<> " " <> commits_get_subject commits ah
684 Edit ah
-> "edit " <> ah
<> " " <> commits_get_subject commits ah
685 Fixup ah
-> "fixup " <> ah
<> " " <> commits_get_subject commits ah
686 Reset tgt
-> "reset " <> tgt
687 Exec
(regex_match
"\\n" -> Just _
) -> error "multiline command canot be saved"
688 Exec cmd
-> "exec " <> cmd
689 Comment cmt
-> string_from_todo_comment cmt
690 Merge ref ps ours noff
->
692 <> (if ours
then " --ours" else "")
693 <> (if noff
then " --no-ff" else "")
694 <> maybe "" (" -c " <>) ref
695 <> " " <> ByteString
.intercalate
"," ps
696 <> maybe "" ((" " <>) . commits_get_subject commits
) ref
)
697 Mark mrk
-> ": " <> mrk
698 UserComment cmt
-> "# " <> cmt
702 forM_
tail $ hPutStrLn out
. \case
703 UserComment cmt
-> cmt
704 TailPickWithComment ah msg
705 -> "----- " <> ah
<> " -----\n"
706 <> string_from_todo_comment msg
709 string_from_todo_comment
:: ByteString
-> ByteString
710 string_from_todo_comment cmt
=
711 case regex_match
"[^\\n]\\.[$\\n]|[^\\n]$|[^\\n]#" cmt
of
713 Nothing
-> "comment\n" <> cmt
<> if BC
.last cmt
== '\n' then "" else "\n" <> ".\n"
715 quoted
= "comment " <> BC
.replicate (BC
.length endMark
) '{' <> "\n" <> cmt
<> endMark
<> "\n"
716 endMark
= fix
(\rec p
-> if p `ByteString
.isInfixOf` cmt
then rec
(p
<> "}") else p
) "}}}"
718 data ReadState
= RStCommand | RStDone | RStCommentPlain ByteString | RStCommentQuoted ByteString ByteString
deriving Show
720 read_todo
:: (MonadIO m
, MonadMask m
) => ByteString
-> Commits
-> m
[Step
]
721 read_todo path commits
= do
722 (s
, todo
) <- execRWST
(mapFileLinesM parseLine path
'\n') () RStCommand
724 RStCommand
-> pure todo
726 mode
-> throwM
$ EditError
"Unterminated comment"
731 | Just
[_
, cmt
] <- regex_match
"^#(.*)$" line
-> tell
[UserComment cmt
]
732 | Just _
<- regex_match
"^end$" line
-> put RStDone
733 | Just
(_
: _
: ah
: _
) <- regex_match
"^(f|fixup) (\\@?[0-9a-zA-Z_\\/]+)( .*)?$" line
735 | Just
(_
: _
: ah
: _
) <- regex_match
"^(p|pick) (\\@?[0-9a-zA-Z_\\/]+)( .*)?$" line
737 | Just
(_
: _
: ah
: _
) <- regex_match
"^(e|edit) (\\@?[0-9a-zA-Z_\\/]+)( .*)?$" line
739 | Just
(_
: ah
: _
) <- regex_match
"^reset (\\@?[0-9a-zA-Z_\\/]+)$" line
741 | Just
(_
: _
: cmd
: _
) <- regex_match
"^(x|exec) (.*)$" line
743 | Just _
<- regex_match
"^comment$" line
-> put
$ RStCommentPlain
""
744 | Just
[_
, b
] <- regex_match
"^comment (\\{+)$" line
745 -> put
$ RStCommentQuoted
"" (BC
.length b `BC
.replicate`
'}')
746 | Just
[_
, options
, _
, parents
] <- regex_match
"^merge(( --ours| --no-ff| -c \\@?[0-9a-zA-Z_\\/]+)*) ([^ ]+)" line
748 merge
<- fix
(\rec m l
-> if
749 | ByteString
.null l
-> pure m
750 | Just
[_
, rest
] <- regex_match
"^ --ours( .*)?$" l
-> rec m
{ mergeOurs
= True } rest
751 | Just
[_
, rest
] <- regex_match
"^ --no-ff( .*)?$" l
-> rec m
{ mergeNoff
= True } rest
752 | Just
[_
, ref
, rest
] <- regex_match
"^ -c (\\@?[0-9a-zA-Z_\\/]+)( .*)?$" l
-> rec m
{mergeRef
= Just ref
} rest
753 |
otherwise -> throwM
$ EditError
("Unexpected merge options: " <> l
))
754 (Merge Nothing
(BC
.split ',' parents
) False False)
757 | Just
[_
, mrk
] <- regex_match
"^: (.*)$" line
758 -> maybe (tell
[Mark mrk
])
759 (const $ throwM
(EditError
("Dangerous symbols in mark name: " <> mrk
)))
760 (regex_match
"[^0-9a-zA-Z_]" mrk
)
761 | Just _
<- regex_match
"^[ \\t]*$" line
-> pure
()
763 | Just
[_
, cmt
] <- regex_match
"^# (.*)$" line
-> tell
[UserComment cmt
]
764 | line
== "." -> tell
[Comment cmt0
] >> put RStCommand
765 |
otherwise -> put
$ RStCommentPlain
(cmt0
<> line
<> "\n")
766 RStCommentQuoted cmt0 quote
767 | quote `ByteString
.isSuffixOf` line
-> tell
[Comment
(cmt0
<> ByteString
.take (ByteString
.length line
- ByteString
.length quote
) line
)] >> put RStCommand
768 |
otherwise -> put
$ RStCommentQuoted
(cmt0
<> line
<> "\n") quote
769 RStDone
-> tell
[UserComment line
]
770 mode
-> throwM
$ EditError
("Unexpected line in mode " <> BC
.pack
(show mode
) <> ": " <> line
)
772 commitsEmpty
= Commits Map
.empty Map
.empty
774 returnC x
= ContT
$ const x
776 data FsThreadState
= FsReady | FsFinalizeMergebases | FsWaitChildren | FsDone
deriving Eq
778 data FsThread
= FsThread
{ fsstState
:: FsThreadState
, fsstCurrent
:: Hash
, fsstTodo
:: [Hash
] }
780 data FsWaiter
= FsWaiter
{ fswThread
:: Int, fswLeft
:: Int, fswTodo
:: Set
.Set Hash
}
783 fssThreads
:: Map
.Map
Int FsThread
,
784 fssSchedule
:: [Int],
785 fssNextThreadId
:: Int,
786 fssChildrenWaiters
:: Map
.Map Hash FsWaiter
,
787 fssTerminatingCommits
:: Set
.Set Hash
}
789 find_sequence
:: Map
.Map Hash Entry
-> Hash
-> Hash
-> [Hash
] -> Either ByteString
[Hash
]
790 find_sequence commits from to through
=
791 step
(FS
(Map
.singleton
1 (FsThread FsReady to
[])) [1] 2 Map
.empty Set
.empty)
793 children_num
= Map
.unionsWith
(+)
794 ((Map
.fromList
$ map (,0) (from
: to
: Map
.keys commits
))
795 : map (Map
.fromList
. map (,1) . entryParents
) (Map
.elems commits
))
797 FS
{ fssSchedule
= [] } -> Left
"No path found"
798 s
@(FS ts sc
@(n
: _
) nextId childerWaiters terminatingCommits
)
799 | FsDone
<- fsstState
(ts Map
.! n
) -> Right
$ reverse $ fsstTodo
(ts Map
.! n
)
800 |
otherwise -> case break ((`
elem`
([FsReady
, FsFinalizeMergebases
] :: [FsThreadState
])) . fsstState
. (ts Map
.!)) sc
of
801 (_
, []) -> error "No thread is READY"
802 (scH
, (scC
@((ts Map
.!) -> FsThread curState curHash curTodo
) : scT
))
803 | Set
.member curHash terminatingCommits
-> step s
{ fssSchedule
= scH
++ scT
}
804 | curState
== FsFinalizeMergebases
->
806 ts
' = if children_num Map
.! curHash
== 1
808 else case Map
.lookup curHash childerWaiters
of
810 Just
(FsWaiter
{ fswThread
= waiter
}) ->
811 Map
.adjust
(\ws
-> ws
{ fsstState
= FsFinalizeMergebases
}) waiter ts
812 (new_tasks
, nextId
') = makeParentTasks nextId
813 in step
(FS
(Map
.union (Map
.fromList new_tasks
) ts
')
814 (scH
++ map fst new_tasks
++ scT
)
817 (Set
.insert curHash terminatingCommits
))
820 ts
' = Map
.adjust
(\t -> t
{ fsstState
= FsDone
}) scC ts
821 keepCurrent
= all (`Set
.member` todoSet
) through
822 (new_tasks
, nextId
') = makeParentTasks nextId
823 in step s
{ fssThreads
= Map
.union (Map
.fromList new_tasks
) ts
',
824 fssSchedule
= scH
++ (if keepCurrent
then [scC
] else []) ++ map fst new_tasks
++ scT
,
825 fssNextThreadId
= nextId
' }
826 | children_num Map
.! curHash
> 1 && not (Map
.member curHash childerWaiters
) ->
827 step s
{ fssThreads
= Map
.adjust
(\t -> t
{ fsstState
= FsWaitChildren
}) scC ts
,
828 fssChildrenWaiters
= Map
.insert curHash
829 (FsWaiter scC
((children_num Map
.! curHash
) - 1) todoSet
)
831 | children_num Map
.! curHash
> 1, Just waiter
<- Map
.lookup curHash childerWaiters
, fswLeft waiter
> 0 ->
833 (todo
', todoIdx
') = foldl' (\(t
, i
) h
-> if Set
.member h i
then (t
,i
) else (t
++ [h
], Set
.insert h i
))
834 (fsstTodo
(ts Map
.! (fswThread waiter
)), fswTodo waiter
)
836 left
' = fswLeft waiter
- 1
837 in step s
{ fssThreads
= Map
.adjust
(\t -> t
{fsstTodo
= todo
',
838 fsstState
= if left
' == 0 then FsReady
else fsstState t
})
841 fssChildrenWaiters
= Map
.adjust
(\w
-> w
{ fswLeft
= left
', fswTodo
= todoIdx
' }) curHash childerWaiters
,
842 fssSchedule
= scH
++ scT
}
845 curTodo
' = curTodo
++ [curHash
]
846 (newTasks
, nextId
') = makeParentTasksEx
(\p
-> FsThread FsReady p curTodo
') nextId
847 in step s
{ fssThreads
= Map
.union (Map
.fromList newTasks
) ts
,
848 fssSchedule
= scH
++ map fst newTasks
++ scT
,
849 fssNextThreadId
= nextId
' }
851 todoSet
= Set
.fromList curTodo
852 makeParentTasksEx newThread fromId
=
853 let tasks
= zip [fromId
..] $ map newThread
854 $ maybe [] entryParents
$ Map
.lookup curHash commits
855 id = last (fromId
: map ((+ 1) . fst) tasks
)
857 makeParentTasks
= makeParentTasksEx
(\p
-> FsThread FsFinalizeMergebases p
[])
859 resolve_ahash
:: (MonadReader TE m
, MonadState TS m
) => ByteString
-> m ByteString
860 resolve_ahash ah
= do
861 refs
<- fmap teRefs ask
862 case regex_match
"^@(.*)$" ah
of
864 marks
<- fmap tsMarks get
865 pure
$ maybe (error ("Mark " <> show mrk
<> " not found")) hashString
(Map
.lookup mrk marks
)
866 Nothing
-> pure
$ maybe ah hashString
(Map
.lookup ah refs
)
868 git_no_uncommitted_changes
:: MonadIO m
=> m
Bool
869 git_no_uncommitted_changes
= liftIO
(system "git diff-index --quiet --ignore-submodules HEAD") >>= \case
870 ExitSuccess
-> pure
True
873 retry
:: (MonadMask m
, MonadIO m
) => m x
-> m
(Maybe x
)
874 retry func
= fix
$ \rec
-> do
876 (func
>>= (pure
. Right
))
877 (\(EditError msg
) -> pure
$ Left msg
)
879 Right x
-> pure
(Just x
)
881 liftIO
$ putStrLn ("Error: " <> msg
)
882 liftIO
$ putStrLn "Retry (y/N)?"
886 (GIE
.IOError { GIE
.ioe_type
= GIE
.EOF
})
891 if "y" `ByteString
.isPrefixOf` answer ||
"Y" `ByteString
.isPrefixOf` answer
895 git_fetch_commit_list commits
[] = pure commits
896 git_fetch_commit_list commits unknowns
= do
898 (map hashString
-> us
, usRest
) = Prelude
.splitAt 20 unknowns
899 commits
<- git_fetch_commits
900 ("show -z --no-patch --pretty=format:%H:%h:%T:%P:%B" <> ArgList us
)
902 git_fetch_commit_list commits usRest
905 gitDir
<- readPopen
"git rev-parse --git-dir"
908 git_verify_clean
= do
909 git_no_uncommitted_changes `unlessM`
fail "Not clean working directory"
911 liftIO
(doesFileExist (gitDir
<> "/rebase-apply")) `whenM`
fail "git-am or rebase in progress"
912 liftIO
(doesFileExist (gitDir
<> "/rebase-merge")) `whenM`
fail "rebase in progress"
914 git_get_checkedout_branch
= do
915 head_path
<- liftIO
$ readPopen
"git symbolic-ref -q HEAD"
916 case regex_match
"^refs/heads/(.*)" head_path
of
917 Just
[_
, p
] -> pure p
918 _
-> fail ("Unsupported ref checked-out: " ++ show head_path
)
920 askGitDir
:: MonadReader
(Env a
) m
=> m ByteString
921 askGitDir
= ask
>>= \r -> pure
(envGitDir r
)
923 handleErrors
:: (String -> IO ()) -> (ByteString
-> IO ()) -> (Int -> IO a
) -> IO a
-> IO a
924 handleErrors printCb printBSCb exitCb action
=
925 action `catches`
([Handler catchExpected
, Handler catchIO
, Handler catchAll
] :: [Handler
IO _
])
927 catchExpected
(ExpectedFailure msg
) = do
930 catchAll
(SomeException e
) = do
931 printCb
("Internal error: " ++ show (typeOf e
))
932 printCb
("Message: " ++ displayException e
)
934 catchIO
(e
:: GIE
.IOException
)
935 | GIE
.UserError
<- GIE
.ioe_type e
= do
936 printCb
("Unexpected happened: " ++ GIE
.ioe_description e
)
939 printCb
("IO error: " ++ displayException e
)